passagemath-repl 10.4.62__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. passagemath_repl-10.4.62.data/scripts/sage-cachegrind +25 -0
  2. passagemath_repl-10.4.62.data/scripts/sage-callgrind +16 -0
  3. passagemath_repl-10.4.62.data/scripts/sage-cleaner +230 -0
  4. passagemath_repl-10.4.62.data/scripts/sage-coverage +327 -0
  5. passagemath_repl-10.4.62.data/scripts/sage-eval +14 -0
  6. passagemath_repl-10.4.62.data/scripts/sage-fixdoctests +708 -0
  7. passagemath_repl-10.4.62.data/scripts/sage-inline-fortran +12 -0
  8. passagemath_repl-10.4.62.data/scripts/sage-ipynb2rst +50 -0
  9. passagemath_repl-10.4.62.data/scripts/sage-ipython +16 -0
  10. passagemath_repl-10.4.62.data/scripts/sage-massif +25 -0
  11. passagemath_repl-10.4.62.data/scripts/sage-notebook +267 -0
  12. passagemath_repl-10.4.62.data/scripts/sage-omega +25 -0
  13. passagemath_repl-10.4.62.data/scripts/sage-preparse +302 -0
  14. passagemath_repl-10.4.62.data/scripts/sage-run +27 -0
  15. passagemath_repl-10.4.62.data/scripts/sage-run-cython +10 -0
  16. passagemath_repl-10.4.62.data/scripts/sage-runtests +9 -0
  17. passagemath_repl-10.4.62.data/scripts/sage-startuptime.py +163 -0
  18. passagemath_repl-10.4.62.data/scripts/sage-valgrind +34 -0
  19. passagemath_repl-10.4.62.dist-info/METADATA +77 -0
  20. passagemath_repl-10.4.62.dist-info/RECORD +162 -0
  21. passagemath_repl-10.4.62.dist-info/WHEEL +5 -0
  22. passagemath_repl-10.4.62.dist-info/top_level.txt +1 -0
  23. sage/all__sagemath_repl.py +119 -0
  24. sage/doctest/__init__.py +4 -0
  25. sage/doctest/__main__.py +236 -0
  26. sage/doctest/all.py +4 -0
  27. sage/doctest/check_tolerance.py +261 -0
  28. sage/doctest/control.py +1727 -0
  29. sage/doctest/external.py +534 -0
  30. sage/doctest/fixtures.py +383 -0
  31. sage/doctest/forker.py +2665 -0
  32. sage/doctest/marked_output.py +102 -0
  33. sage/doctest/parsing.py +1708 -0
  34. sage/doctest/parsing_test.py +79 -0
  35. sage/doctest/reporting.py +733 -0
  36. sage/doctest/rif_tol.py +124 -0
  37. sage/doctest/sources.py +1657 -0
  38. sage/doctest/test.py +584 -0
  39. sage/doctest/tests/1second.rst +4 -0
  40. sage/doctest/tests/99seconds.rst +4 -0
  41. sage/doctest/tests/abort.rst +5 -0
  42. sage/doctest/tests/atexit.rst +7 -0
  43. sage/doctest/tests/fail_and_die.rst +6 -0
  44. sage/doctest/tests/initial.rst +15 -0
  45. sage/doctest/tests/interrupt.rst +7 -0
  46. sage/doctest/tests/interrupt_diehard.rst +14 -0
  47. sage/doctest/tests/keyboardinterrupt.rst +11 -0
  48. sage/doctest/tests/longtime.rst +5 -0
  49. sage/doctest/tests/nodoctest +5 -0
  50. sage/doctest/tests/random_seed.rst +4 -0
  51. sage/doctest/tests/show_skipped.rst +18 -0
  52. sage/doctest/tests/sig_on.rst +9 -0
  53. sage/doctest/tests/simple_failure.rst +8 -0
  54. sage/doctest/tests/sleep_and_raise.rst +106 -0
  55. sage/doctest/tests/tolerance.rst +31 -0
  56. sage/doctest/util.py +750 -0
  57. sage/interfaces/cleaner.py +48 -0
  58. sage/interfaces/quit.py +163 -0
  59. sage/misc/all__sagemath_repl.py +51 -0
  60. sage/misc/banner.py +235 -0
  61. sage/misc/benchmark.py +221 -0
  62. sage/misc/classgraph.py +131 -0
  63. sage/misc/copying.py +22 -0
  64. sage/misc/cython.py +694 -0
  65. sage/misc/dev_tools.py +745 -0
  66. sage/misc/edit_module.py +304 -0
  67. sage/misc/explain_pickle.py +3079 -0
  68. sage/misc/gperftools.py +361 -0
  69. sage/misc/inline_fortran.py +212 -0
  70. sage/misc/messaging.py +86 -0
  71. sage/misc/pager.py +21 -0
  72. sage/misc/profiler.py +179 -0
  73. sage/misc/python.py +70 -0
  74. sage/misc/remote_file.py +53 -0
  75. sage/misc/sage_eval.py +246 -0
  76. sage/misc/sage_input.py +3621 -0
  77. sage/misc/sagedoc.py +1742 -0
  78. sage/misc/sh.py +38 -0
  79. sage/misc/trace.py +90 -0
  80. sage/repl/__init__.py +16 -0
  81. sage/repl/all.py +15 -0
  82. sage/repl/attach.py +625 -0
  83. sage/repl/configuration.py +186 -0
  84. sage/repl/display/__init__.py +1 -0
  85. sage/repl/display/fancy_repr.py +354 -0
  86. sage/repl/display/formatter.py +318 -0
  87. sage/repl/display/jsmol_iframe.py +290 -0
  88. sage/repl/display/pretty_print.py +153 -0
  89. sage/repl/display/util.py +163 -0
  90. sage/repl/image.py +302 -0
  91. sage/repl/inputhook.py +91 -0
  92. sage/repl/interface_magic.py +298 -0
  93. sage/repl/interpreter.py +854 -0
  94. sage/repl/ipython_extension.py +593 -0
  95. sage/repl/ipython_kernel/__init__.py +1 -0
  96. sage/repl/ipython_kernel/__main__.py +4 -0
  97. sage/repl/ipython_kernel/all_jupyter.py +10 -0
  98. sage/repl/ipython_kernel/install.py +301 -0
  99. sage/repl/ipython_kernel/interact.py +278 -0
  100. sage/repl/ipython_kernel/kernel.py +217 -0
  101. sage/repl/ipython_kernel/widgets.py +466 -0
  102. sage/repl/ipython_kernel/widgets_sagenb.py +587 -0
  103. sage/repl/ipython_tests.py +163 -0
  104. sage/repl/load.py +326 -0
  105. sage/repl/preparse.py +2218 -0
  106. sage/repl/prompts.py +90 -0
  107. sage/repl/rich_output/__init__.py +4 -0
  108. sage/repl/rich_output/backend_base.py +648 -0
  109. sage/repl/rich_output/backend_doctest.py +316 -0
  110. sage/repl/rich_output/backend_emacs.py +151 -0
  111. sage/repl/rich_output/backend_ipython.py +596 -0
  112. sage/repl/rich_output/buffer.py +311 -0
  113. sage/repl/rich_output/display_manager.py +829 -0
  114. sage/repl/rich_output/example.avi +0 -0
  115. sage/repl/rich_output/example.canvas3d +1 -0
  116. sage/repl/rich_output/example.dvi +0 -0
  117. sage/repl/rich_output/example.flv +0 -0
  118. sage/repl/rich_output/example.gif +0 -0
  119. sage/repl/rich_output/example.jpg +0 -0
  120. sage/repl/rich_output/example.mkv +0 -0
  121. sage/repl/rich_output/example.mov +0 -0
  122. sage/repl/rich_output/example.mp4 +0 -0
  123. sage/repl/rich_output/example.ogv +0 -0
  124. sage/repl/rich_output/example.pdf +0 -0
  125. sage/repl/rich_output/example.png +0 -0
  126. sage/repl/rich_output/example.svg +54 -0
  127. sage/repl/rich_output/example.webm +0 -0
  128. sage/repl/rich_output/example.wmv +0 -0
  129. sage/repl/rich_output/example_jmol.spt.zip +0 -0
  130. sage/repl/rich_output/example_wavefront_scene.mtl +7 -0
  131. sage/repl/rich_output/example_wavefront_scene.obj +17 -0
  132. sage/repl/rich_output/output_basic.py +391 -0
  133. sage/repl/rich_output/output_browser.py +103 -0
  134. sage/repl/rich_output/output_catalog.py +54 -0
  135. sage/repl/rich_output/output_graphics.py +320 -0
  136. sage/repl/rich_output/output_graphics3d.py +345 -0
  137. sage/repl/rich_output/output_video.py +231 -0
  138. sage/repl/rich_output/preferences.py +432 -0
  139. sage/repl/rich_output/pretty_print.py +339 -0
  140. sage/repl/rich_output/test_backend.py +201 -0
  141. sage/repl/user_globals.py +214 -0
  142. sage/tests/__init__.py +1 -0
  143. sage/tests/all.py +3 -0
  144. sage/tests/article_heuberger_krenn_kropf_fsm-in-sage.py +630 -0
  145. sage/tests/arxiv_0812_2725.py +351 -0
  146. sage/tests/benchmark.py +1923 -0
  147. sage/tests/book_schilling_zabrocki_kschur_primer.py +795 -0
  148. sage/tests/book_stein_ent.py +651 -0
  149. sage/tests/book_stein_modform.py +558 -0
  150. sage/tests/cmdline.py +790 -0
  151. sage/tests/combinatorial_hopf_algebras.py +52 -0
  152. sage/tests/finite_poset.py +623 -0
  153. sage/tests/functools_partial_src.py +27 -0
  154. sage/tests/gosper-sum.py +218 -0
  155. sage/tests/lazy_imports.py +28 -0
  156. sage/tests/modular_group_cohomology.py +80 -0
  157. sage/tests/numpy.py +21 -0
  158. sage/tests/parigp.py +76 -0
  159. sage/tests/startup.py +27 -0
  160. sage/tests/symbolic-series.py +76 -0
  161. sage/tests/sympy.py +16 -0
  162. sage/tests/test_deprecation.py +31 -0
@@ -0,0 +1,1708 @@
1
+ # sage_setup: distribution = sagemath-repl
2
+ """
3
+ Parsing docstrings
4
+
5
+ This module contains functions and classes that parse docstrings.
6
+
7
+ AUTHORS:
8
+
9
+ - David Roe (2012-03-27) -- initial version, based on Robert Bradshaw's code.
10
+
11
+ - Jeroen Demeyer(2014-08-28) -- much improved handling of tolerances
12
+ using interval arithmetic (:issue:`16889`).
13
+ """
14
+
15
+ # ****************************************************************************
16
+ # Copyright (C) 2012-2018 David Roe <roed.math@gmail.com>
17
+ # 2012 Robert Bradshaw <robertwb@gmail.com>
18
+ # 2012 William Stein <wstein@gmail.com>
19
+ # 2013 R. Andrew Ohana
20
+ # 2013 Volker Braun
21
+ # 2013-2018 Jeroen Demeyer <jdemeyer@cage.ugent.be>
22
+ # 2016-2021 Frédéric Chapoton
23
+ # 2017-2018 Erik M. Bray
24
+ # 2020 Marc Mezzarobba
25
+ # 2020-2023 Matthias Koeppe
26
+ # 2022 John H. Palmieri
27
+ # 2022 Sébastien Labbé
28
+ # 2023 Kwankyu Lee
29
+ #
30
+ # Distributed under the terms of the GNU General Public License (GPL)
31
+ # as published by the Free Software Foundation; either version 2 of
32
+ # the License, or (at your option) any later version.
33
+ # https://www.gnu.org/licenses/
34
+ # ****************************************************************************
35
+
36
+ import collections.abc
37
+ import doctest
38
+ import platform
39
+ import re
40
+ from collections import defaultdict
41
+ from functools import reduce
42
+ from typing import Literal, Union, overload
43
+
44
+ from sage.misc.cachefunc import cached_function
45
+ 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
+
54
+
55
+ # This is the correct pattern to match ISO/IEC 6429 ANSI escape sequences:
56
+ ansi_escape_sequence = re.compile(r"(\x1b[@-Z\\-~]|\x1b\[.*?[@-~]|\x9b.*?[@-~])")
57
+
58
+ special_optional_regex = (
59
+ "py2|long time|not implemented|not tested|optional|needs|known bug"
60
+ )
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*)*)",
64
+ re.IGNORECASE,
65
+ )
66
+ special_optional_regex = re.compile(special_optional_regex, re.IGNORECASE)
67
+ tag_with_explanation_regex = re.compile(tag_with_explanation_regex, re.IGNORECASE)
68
+
69
+ nodoctest_regex = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*nodoctest')
70
+ optionaltag_regex = re.compile(r"^(\w|[.])+$")
71
+ optionalfiledirective_regex = re.compile(
72
+ r'\s*(#+|%+|r"+|"+|\.\.)\s*sage\.doctest: (.*)'
73
+ )
74
+
75
+
76
+ @overload
77
+ def parse_optional_tags(string: str) -> dict[str, Union[str, None]]:
78
+ pass
79
+
80
+
81
+ @overload
82
+ def parse_optional_tags(
83
+ string: str, *, return_string_sans_tags: Literal[True]
84
+ ) -> tuple[dict[str, Union[str, None]], str, bool]:
85
+ pass
86
+
87
+
88
+ def parse_optional_tags(
89
+ string: str, *, return_string_sans_tags: bool = False
90
+ ) -> Union[tuple[dict[str, Union[str, None]], str, bool], dict[str, Union[str, None]]]:
91
+ r"""
92
+ Return a dictionary whose keys are optional tags from the following
93
+ set that occur in a comment on the first line of the input string.
94
+
95
+ - ``'long time'``
96
+ - ``'not implemented'``
97
+ - ``'not tested'``
98
+ - ``'known bug'`` (possible values are ``None``, ``linux`` and ``macos``)
99
+ - ``'py2'``
100
+ - ``'optional -- FEATURE...'`` or ``'needs FEATURE...'`` --
101
+ the dictionary will just have the key ``'FEATURE'``
102
+
103
+ The values, if non-``None``, are strings with optional explanations
104
+ for a tag, which may appear in parentheses after the tag in ``string``.
105
+
106
+ INPUT:
107
+
108
+ - ``string`` -- string
109
+
110
+ - ``return_string_sans_tags`` -- boolean (default: ``False``); whether to
111
+ additionally return ``string`` with the optional tags removed but other
112
+ comments kept and a boolean ``is_persistent``
113
+
114
+ EXAMPLES::
115
+
116
+ sage: from sage.doctest.parsing import parse_optional_tags
117
+ sage: parse_optional_tags("sage: magma('2 + 2')# optional: magma")
118
+ {'magma': None}
119
+ sage: parse_optional_tags("sage: #optional -- mypkg")
120
+ {'mypkg': None}
121
+ sage: parse_optional_tags("sage: print(1) # parentheses are optional here")
122
+ {}
123
+ sage: parse_optional_tags("sage: print(1) # optional")
124
+ {}
125
+ sage: sorted(list(parse_optional_tags("sage: #optional -- foo bar, baz")))
126
+ ['bar', 'foo']
127
+ sage: parse_optional_tags("sage: #optional -- foo.bar, baz")
128
+ {'foo.bar': None}
129
+ sage: parse_optional_tags("sage: #needs foo.bar, baz")
130
+ {'foo.bar': None}
131
+ sage: sorted(list(parse_optional_tags(" sage: factor(10^(10^10) + 1) # LoNg TiME, NoT TeSTED; OptioNAL -- P4cka9e")))
132
+ ['long time', 'not tested', 'p4cka9e']
133
+ sage: parse_optional_tags(" sage: raise RuntimeError # known bug")
134
+ {'bug': None}
135
+ sage: sorted(list(parse_optional_tags(" sage: determine_meaning_of_life() # long time, not implemented")))
136
+ ['long time', 'not implemented']
137
+
138
+ We don't parse inside strings::
139
+
140
+ sage: parse_optional_tags(" sage: print(' # long time')")
141
+ {}
142
+ sage: parse_optional_tags(" sage: print(' # long time') # not tested")
143
+ {'not tested': None}
144
+
145
+ UTF-8 works::
146
+
147
+ sage: parse_optional_tags("'ěščřžýáíéďĎ'")
148
+ {}
149
+
150
+ Tags with parenthesized explanations::
151
+
152
+ sage: parse_optional_tags(" sage: 1 + 1 # long time (1 year, 2 months??), optional - bliss (because)")
153
+ {'bliss': 'because', 'long time': '1 year, 2 months??'}
154
+
155
+ With ``return_string_sans_tags=True``::
156
+
157
+ sage: parse_optional_tags("sage: print(1) # very important 1 # optional - foo",
158
+ ....: return_string_sans_tags=True)
159
+ ({'foo': None}, 'sage: print(1) # very important 1 ', False)
160
+ sage: parse_optional_tags("sage: print( # very important too # optional - foo\n....: 2)",
161
+ ....: return_string_sans_tags=True)
162
+ ({'foo': None}, 'sage: print( # very important too \n....: 2)', False)
163
+ sage: parse_optional_tags("sage: #this is persistent #needs scipy",
164
+ ....: return_string_sans_tags=True)
165
+ ({'scipy': None}, 'sage: #this is persistent ', True)
166
+ sage: parse_optional_tags("sage: #this is not #needs scipy\n....: import scipy",
167
+ ....: return_string_sans_tags=True)
168
+ ({'scipy': None}, 'sage: #this is not \n....: import scipy', False)
169
+ """
170
+ safe, literals, state = strip_string_literals(string)
171
+ split = safe.split('\n', 1)
172
+ if len(split) > 1:
173
+ first_line, rest = split
174
+ else:
175
+ first_line, rest = split[0], None
176
+
177
+ sharp_index = first_line.find('#')
178
+ if sharp_index < 0: # no comment
179
+ if return_string_sans_tags:
180
+ return {}, string, False
181
+ else:
182
+ return {}
183
+
184
+ first_line_sans_comments, comment = first_line[:sharp_index] % literals, first_line[sharp_index:] % literals
185
+ if not first_line_sans_comments.endswith(" ") and not first_line_sans_comments.rstrip().endswith("sage:"):
186
+ # Enforce two spaces before comment
187
+ first_line_sans_comments = first_line_sans_comments.rstrip() + " "
188
+
189
+ if return_string_sans_tags:
190
+ # skip non-tag comments that precede the first tag comment
191
+ if m := optional_regex.search(comment):
192
+ sharp_index = comment[:m.start(0) + 1].rfind('#')
193
+ if sharp_index >= 0:
194
+ first_line = first_line_sans_comments + comment[:sharp_index]
195
+ comment = comment[sharp_index:]
196
+ else:
197
+ # no tag comment
198
+ return {}, string, False
199
+
200
+ tags: dict[str, Union[str, None]] = {}
201
+ for m in optional_regex.finditer(comment):
202
+ cmd = m.group("cmd").lower().strip()
203
+ if cmd == "":
204
+ # skip empty tags
205
+ continue
206
+ if cmd == "known bug":
207
+ value = None
208
+ if m.groups("tags") and m.group("tags").strip().lower().startswith("linux"):
209
+ value = "linux"
210
+ if m.groups("tags") and m.group("tags").strip().lower().startswith("macos"):
211
+ value = "macos"
212
+
213
+ # rename 'known bug' to 'bug' so that such tests will be run by sage -t ... -only-optional=bug
214
+ tags["bug"] = value
215
+ elif cmd not in ["optional", "needs"]:
216
+ tags[cmd] = m.group("cmd_explanation") or None
217
+ else:
218
+ # other tags with additional values
219
+ tags_with_value = {
220
+ m.group(1).lower().strip(): m.group(2) or None
221
+ for m in tag_with_explanation_regex.finditer(m.group("tags"))
222
+ }
223
+ tags_with_value.pop("", None)
224
+ tags.update(tags_with_value)
225
+
226
+ if return_string_sans_tags:
227
+ is_persistent = tags and first_line_sans_comments.strip() == 'sage:' and not rest # persistent (block-scoped) tag
228
+ return tags, (first_line + '\n' + rest % literals if rest is not None
229
+ else first_line), is_persistent
230
+ else:
231
+ return tags
232
+
233
+
234
+ def parse_file_optional_tags(lines):
235
+ r"""
236
+ Scan the first few lines for file-level doctest directives.
237
+
238
+ INPUT:
239
+
240
+ - ``lines`` -- iterable of pairs ``(lineno, line)``
241
+
242
+ OUTPUT: dictionary whose keys are strings (tags);
243
+ see :func:`parse_optional_tags`
244
+
245
+ EXAMPLES::
246
+
247
+ sage: from sage.doctest.parsing import parse_file_optional_tags
248
+ sage: filename = tmp_filename(ext='.pyx')
249
+ sage: with open(filename, "r") as f:
250
+ ....: parse_file_optional_tags(enumerate(f))
251
+ {}
252
+ sage: with open(filename, "w") as f:
253
+ ....: _ = f.write("# nodoctest")
254
+ sage: with open(filename, "r") as f:
255
+ ....: parse_file_optional_tags(enumerate(f))
256
+ {'not tested': None}
257
+ sage: with open(filename, "w") as f:
258
+ ....: _ = f.write("# sage.doctest: " # broken in two source lines to avoid the pattern
259
+ ....: "optional - xyz") # of relint (multiline_doctest_comment)
260
+ sage: with open(filename, "r") as f:
261
+ ....: parse_file_optional_tags(enumerate(f))
262
+ {'xyz': None}
263
+ """
264
+ tags = {}
265
+ for line_count, line in lines:
266
+ if nodoctest_regex.match(line):
267
+ tags['not tested'] = None
268
+ if m := optionalfiledirective_regex.match(line):
269
+ file_tag_string = m.group(2)
270
+ tags.update(parse_optional_tags('#' + file_tag_string))
271
+ if line_count >= 10:
272
+ break
273
+ return tags
274
+
275
+
276
+ @cached_function
277
+ def _standard_tags():
278
+ r"""
279
+ Return the set of the names of all standard features.
280
+
281
+ EXAMPLES::
282
+
283
+ sage: from sage.doctest.parsing import _standard_tags
284
+ sage: sorted(_standard_tags())
285
+ [..., 'numpy', ..., 'sage.rings.finite_rings', ...]
286
+ """
287
+ from sage.features.all import all_features
288
+ return frozenset(feature.name for feature in all_features()
289
+ if feature._spkg_type() == 'standard')
290
+
291
+
292
+ def _tag_group(tag):
293
+ r"""
294
+ Classify a doctest tag as belonging to one of 4 groups.
295
+
296
+ INPUT:
297
+
298
+ - ``tag`` -- string
299
+
300
+ OUTPUT: string; one of ``'special'``, ``'optional'``, ``'standard'``, ``'sage'``
301
+
302
+ EXAMPLES::
303
+
304
+ sage: from sage.doctest.parsing import _tag_group
305
+ sage: _tag_group('scipy')
306
+ 'standard'
307
+ sage: _tag_group('sage.numerical.mip')
308
+ 'sage'
309
+ sage: _tag_group('bliss')
310
+ 'optional'
311
+ sage: _tag_group('not tested')
312
+ 'special'
313
+ """
314
+ if tag.startswith('sage.'):
315
+ return 'sage'
316
+ elif tag in _standard_tags():
317
+ return 'standard'
318
+ elif not special_optional_regex.fullmatch(tag):
319
+ return 'optional'
320
+ else:
321
+ return 'special'
322
+
323
+
324
+ def unparse_optional_tags(tags, prefix='# '):
325
+ r"""
326
+ Return a comment string that sets ``tags``.
327
+
328
+ INPUT:
329
+
330
+ - ``tags`` -- dictionary or iterable of tags, as output by
331
+ :func:`parse_optional_tags`
332
+
333
+ - ``prefix`` -- to be put before a nonempty string
334
+
335
+ EXAMPLES::
336
+
337
+ sage: from sage.doctest.parsing import unparse_optional_tags
338
+ sage: unparse_optional_tags({})
339
+ ''
340
+ sage: unparse_optional_tags({'magma': None})
341
+ '# optional - magma'
342
+ sage: unparse_optional_tags({'fictional_optional': None,
343
+ ....: 'sage.rings.number_field': None,
344
+ ....: 'scipy': 'just because',
345
+ ....: 'bliss': None})
346
+ '# optional - bliss fictional_optional, needs scipy (just because) sage.rings.number_field'
347
+ sage: unparse_optional_tags(['long time', 'not tested', 'p4cka9e'], prefix='')
348
+ 'long time, not tested, optional - p4cka9e'
349
+ """
350
+ group = defaultdict(set)
351
+ if isinstance(tags, collections.abc.Mapping):
352
+ for tag, explanation in tags.items():
353
+ if tag == 'bug':
354
+ tag = 'known bug'
355
+ group[_tag_group(tag)].add(f'{tag} ({explanation})' if explanation else tag)
356
+ else:
357
+ for tag in tags:
358
+ if tag == 'bug':
359
+ tag = 'known bug'
360
+ group[_tag_group(tag)].add(tag)
361
+
362
+ tags = sorted(group.pop('special', []))
363
+ if 'optional' in group:
364
+ tags.append('optional - ' + " ".join(sorted(group.pop('optional'))))
365
+ if 'standard' in group or 'sage' in group:
366
+ tags.append('needs ' + " ".join(sorted(group.pop('standard', []))
367
+ + sorted(group.pop('sage', []))))
368
+ assert not group
369
+ if tags:
370
+ return prefix + ', '.join(tags)
371
+ return ''
372
+
373
+
374
+ optional_tag_columns = [48, 56, 64, 72, 80, 84]
375
+ standard_tag_columns = [88, 100, 120, 160]
376
+
377
+
378
+ def update_optional_tags(line, tags=None, *, add_tags=None, remove_tags=None, force_rewrite=False):
379
+ r"""
380
+ Return the doctest ``line`` with tags changed.
381
+
382
+ EXAMPLES::
383
+
384
+ sage: from sage.doctest.parsing import update_optional_tags, optional_tag_columns, standard_tag_columns
385
+ sage: ruler = ''
386
+ sage: for column in optional_tag_columns:
387
+ ....: ruler += ' ' * (column - len(ruler)) + 'V'
388
+ sage: for column in standard_tag_columns:
389
+ ....: ruler += ' ' * (column - len(ruler)) + 'v'
390
+ sage: def print_with_ruler(lines):
391
+ ....: print('|' + ruler)
392
+ ....: for line in lines:
393
+ ....: print('|' + line)
394
+ sage: print_with_ruler([ # the tags are obscured in the source file to avoid relint warnings
395
+ ....: update_optional_tags(' sage: something() # opt' 'ional - latte_int',
396
+ ....: remove_tags=['latte_int', 'wasnt_even_there']),
397
+ ....: update_optional_tags(' sage: nothing_to_be_seen_here()',
398
+ ....: tags=['scipy', 'long time']),
399
+ ....: update_optional_tags(' sage: nothing_to_be_seen_here(honestly=True)',
400
+ ....: add_tags=['scipy', 'long time']),
401
+ ....: update_optional_tags(' sage: nothing_to_be_seen_here(honestly=True, very=True)',
402
+ ....: add_tags=['scipy', 'long time']),
403
+ ....: update_optional_tags(' sage: no_there_is_absolutely_nothing_to_be_seen_here_i_am_serious()#opt' 'ional:bliss',
404
+ ....: add_tags=['scipy', 'long time']),
405
+ ....: update_optional_tags(' sage: ntbsh() # abbrv for above#opt' 'ional:bliss',
406
+ ....: add_tags={'scipy': None, 'long time': '30s on the highest setting'}),
407
+ ....: update_optional_tags(' sage: no_there_is_absolutely_nothing_to_be_seen_here_i_am_serious() # really, you can trust me here',
408
+ ....: add_tags=['scipy']),
409
+ ....: ])
410
+ | V V V V V V v v v v
411
+ | sage: something()
412
+ | sage: nothing_to_be_seen_here() # long time # needs scipy
413
+ | sage: nothing_to_be_seen_here(honestly=True) # long time # needs scipy
414
+ | sage: nothing_to_be_seen_here(honestly=True, very=True) # long time # needs scipy
415
+ | sage: no_there_is_absolutely_nothing_to_be_seen_here_i_am_serious() # long time, optional - bliss, needs scipy
416
+ | sage: ntbsh() # abbrv for above # long time (30s on the highest setting), optional - bliss, needs scipy
417
+ | sage: no_there_is_absolutely_nothing_to_be_seen_here_i_am_serious() # really, you can trust me here # needs scipy
418
+
419
+ When no tags are changed, by default, the unchanged input is returned.
420
+ We can force a rewrite; unconditionally or whenever standard tags are involved.
421
+ But even when forced, if comments are already aligned at one of the standard alignment columns,
422
+ this alignment is kept even if we would normally realign farther to the left::
423
+
424
+ sage: print_with_ruler([
425
+ ....: update_optional_tags(' sage: unforced() # opt' 'ional - latte_int'),
426
+ ....: update_optional_tags(' sage: unforced() # opt' 'ional - latte_int',
427
+ ....: add_tags=['latte_int']),
428
+ ....: update_optional_tags(' sage: forced()#opt' 'ional- latte_int',
429
+ ....: force_rewrite=True),
430
+ ....: update_optional_tags(' sage: forced() # opt' 'ional - scipy',
431
+ ....: force_rewrite='standard'),
432
+ ....: update_optional_tags(' sage: aligned_with_below() # opt' 'ional - 4ti2',
433
+ ....: force_rewrite=True),
434
+ ....: update_optional_tags(' sage: aligned_with_above() # opt' 'ional - 4ti2',
435
+ ....: force_rewrite=True),
436
+ ....: update_optional_tags(' sage: also_already_aligned() # ne' 'eds scipy',
437
+ ....: force_rewrite='standard'),
438
+ ....: update_optional_tags(' sage: two_columns_first_preserved() # lo' 'ng time # ne' 'eds scipy',
439
+ ....: force_rewrite='standard'),
440
+ ....: update_optional_tags(' sage: two_columns_first_preserved() # lo' 'ng time # ne' 'eds scipy',
441
+ ....: force_rewrite='standard'),
442
+ ....: ])
443
+ | V V V V V V v v v v
444
+ | sage: unforced() # optional - latte_int
445
+ | sage: unforced() # optional - latte_int
446
+ | sage: forced() # optional - latte_int
447
+ | sage: forced() # needs scipy
448
+ | sage: aligned_with_below() # optional - 4ti2
449
+ | sage: aligned_with_above() # optional - 4ti2
450
+ | sage: also_already_aligned() # needs scipy
451
+ | sage: two_columns_first_preserved() # long time # needs scipy
452
+ | sage: two_columns_first_preserved() # long time # needs scipy
453
+
454
+ Rewriting a persistent (block-scoped) tag::
455
+
456
+ sage: print_with_ruler([
457
+ ....: update_optional_tags(' sage: #opt' 'ional:magma sage.symbolic',
458
+ ....: force_rewrite=True),
459
+ ....: ])
460
+ | V V V V V V v v v v
461
+ | sage: # optional - magma, needs sage.symbolic
462
+ """
463
+ if not re.match('( *sage: *)(.*)', line):
464
+ raise ValueError(f'line must start with a sage: prompt, got: {line}')
465
+
466
+ current_tags, line_sans_tags, is_persistent = parse_optional_tags(line.rstrip(), return_string_sans_tags=True)
467
+
468
+ if isinstance(tags, collections.abc.Mapping):
469
+ new_tags = dict(tags)
470
+ elif tags is not None:
471
+ new_tags = {tag: None for tag in tags}
472
+ else:
473
+ new_tags = dict(current_tags)
474
+
475
+ if add_tags is not None:
476
+ if isinstance(add_tags, collections.abc.Mapping):
477
+ new_tags.update(add_tags)
478
+ else:
479
+ new_tags.update({tag: None for tag in add_tags})
480
+
481
+ if remove_tags is not None:
482
+ for tag in remove_tags:
483
+ new_tags.pop(tag, None)
484
+
485
+ if not force_rewrite and new_tags == current_tags:
486
+ return line
487
+
488
+ if not new_tags:
489
+ return line_sans_tags.rstrip()
490
+
491
+ if (force_rewrite == 'standard'
492
+ and new_tags == current_tags
493
+ and not any(_tag_group(tag) in ['standard', 'sage']
494
+ for tag in new_tags)):
495
+ return line
496
+
497
+ if is_persistent:
498
+ line = line_sans_tags.rstrip() + ' '
499
+ else:
500
+ group = defaultdict(set)
501
+ for tag in new_tags:
502
+ group[_tag_group(tag)].add(tag)
503
+ tag_columns = (optional_tag_columns if group['optional'] or group['special']
504
+ else standard_tag_columns)
505
+
506
+ if len(line_sans_tags) in tag_columns and line_sans_tags[-2:] == ' ':
507
+ # keep alignment
508
+ line = line_sans_tags
509
+ pass
510
+ else:
511
+ # realign
512
+ line = line_sans_tags.rstrip()
513
+ for column in tag_columns:
514
+ if len(line) <= column - 2:
515
+ line += ' ' * (column - 2 - len(line))
516
+ break
517
+ line += ' '
518
+
519
+ if (group['optional'] or group['special']) and (group['standard'] or group['sage']):
520
+ # Try if two-column mode works better
521
+ first_part = unparse_optional_tags({tag: explanation
522
+ for tag, explanation in new_tags.items()
523
+ if (tag in group['optional']
524
+ or tag in group['special'])})
525
+ column = standard_tag_columns[0]
526
+ if len(line + first_part) + 8 <= column:
527
+ line += first_part
528
+ line += ' ' * (column - len(line))
529
+ line += unparse_optional_tags({tag: explanation
530
+ for tag, explanation in new_tags.items()
531
+ if not (tag in group['optional']
532
+ or tag in group['special'])})
533
+ return line.rstrip()
534
+
535
+ line += unparse_optional_tags(new_tags)
536
+ return line
537
+
538
+
539
+ def parse_tolerance(source, want):
540
+ r"""
541
+ Return a version of ``want`` marked up with the tolerance tags
542
+ specified in ``source``.
543
+
544
+ INPUT:
545
+
546
+ - ``source`` -- string, the source of a doctest
547
+ - ``want`` -- string, the desired output of the doctest
548
+
549
+ OUTPUT: ``want`` if there are no tolerance tags specified; a
550
+ :class:`MarkedOutput` version otherwise
551
+
552
+ EXAMPLES::
553
+
554
+ sage: from sage.doctest.parsing import parse_tolerance
555
+ sage: marked = parse_tolerance("sage: s.update(abs_tol = .0000001)", "")
556
+ sage: type(marked)
557
+ <class 'str'>
558
+ sage: marked = parse_tolerance("sage: s.update(tol = 0.1); s.rel_tol # abs tol 0.01 ", "")
559
+ sage: marked.tol
560
+ 0
561
+ sage: marked.rel_tol
562
+ 0
563
+ sage: marked.abs_tol # needs sage.rings.real_mpfr
564
+ 0.010000000000000000000...?
565
+ """
566
+ # regular expressions
567
+ random_marker = re.compile('.*random', re.I)
568
+ tolerance_pattern = re.compile(r'\b((?:abs(?:olute)?)|(?:rel(?:ative)?))? *?tol(?:erance)?\b( +[0-9.e+-]+)?')
569
+
570
+ safe, literals, state = strip_string_literals(source)
571
+ first_line = safe.split('\n', 1)[0]
572
+ if '#' not in first_line:
573
+ return want
574
+ comment = first_line[first_line.find('#') + 1:]
575
+ comment = comment[comment.index('(') + 1: comment.rindex(')')]
576
+ # strip_string_literals replaces comments
577
+ comment = literals[comment]
578
+ if random_marker.search(comment):
579
+ want = MarkedOutput(want).update(random=True)
580
+ else:
581
+ m = tolerance_pattern.search(comment)
582
+ if m:
583
+ rel_or_abs, epsilon = m.groups()
584
+ if epsilon is None:
585
+ epsilon = RIFtol("1e-15")
586
+ else:
587
+ epsilon = RIFtol(epsilon)
588
+ if rel_or_abs is None:
589
+ want = MarkedOutput(want).update(tol=epsilon)
590
+ elif rel_or_abs.startswith('rel'):
591
+ want = MarkedOutput(want).update(rel_tol=epsilon)
592
+ elif rel_or_abs.startswith('abs'):
593
+ want = MarkedOutput(want).update(abs_tol=epsilon)
594
+ else:
595
+ raise RuntimeError
596
+ return want
597
+
598
+
599
+ def pre_hash(s):
600
+ """
601
+ Prepends a string with its length.
602
+
603
+ EXAMPLES::
604
+
605
+ sage: from sage.doctest.parsing import pre_hash
606
+ sage: pre_hash("abc")
607
+ '3:abc'
608
+ """
609
+ return "%s:%s" % (len(s), s)
610
+
611
+
612
+ def get_source(example):
613
+ """
614
+ Return the source with the leading 'sage: ' stripped off.
615
+
616
+ EXAMPLES::
617
+
618
+ sage: from sage.doctest.parsing import get_source
619
+ sage: from sage.doctest.sources import DictAsObject
620
+ sage: example = DictAsObject({})
621
+ sage: example.sage_source = "2 + 2"
622
+ sage: example.source = "sage: 2 + 2"
623
+ sage: get_source(example)
624
+ '2 + 2'
625
+ sage: example = DictAsObject({})
626
+ sage: example.source = "3 + 3"
627
+ sage: get_source(example)
628
+ '3 + 3'
629
+ """
630
+ return getattr(example, 'sage_source', example.source)
631
+
632
+
633
+ def reduce_hex(fingerprints):
634
+ """
635
+ Return a symmetric function of the arguments as hex strings.
636
+
637
+ The arguments should be 32 character strings consisting of hex
638
+ digits: 0-9 and a-f.
639
+
640
+ EXAMPLES::
641
+
642
+ sage: from sage.doctest.parsing import reduce_hex
643
+ sage: reduce_hex(["abc", "12399aedf"])
644
+ '0000000000000000000000012399a463'
645
+ sage: reduce_hex(["12399aedf","abc"])
646
+ '0000000000000000000000012399a463'
647
+ """
648
+ from operator import xor
649
+ res = reduce(xor, (int(x, 16) for x in fingerprints), 0)
650
+ if res < 0:
651
+ res += 1 << 128
652
+ return "%032x" % res
653
+
654
+
655
+ class OriginalSource:
656
+ r"""
657
+ Context swapping out the pre-parsed source with the original for
658
+ better reporting.
659
+
660
+ EXAMPLES::
661
+
662
+ sage: from sage.doctest.sources import FileDocTestSource
663
+ sage: from sage.doctest.control import DocTestDefaults
664
+ sage: filename = sage.doctest.forker.__file__
665
+ sage: FDS = FileDocTestSource(filename, DocTestDefaults())
666
+ sage: doctests, extras = FDS.create_doctests(globals())
667
+ sage: ex = doctests[0].examples[0]
668
+ sage: ex.sage_source
669
+ 'doctest_var = 42; doctest_var^2\n'
670
+ sage: ex.source
671
+ 'doctest_var = Integer(42); doctest_var**Integer(2)\n'
672
+ sage: from sage.doctest.parsing import OriginalSource
673
+ sage: with OriginalSource(ex):
674
+ ....: ex.source
675
+ 'doctest_var = 42; doctest_var^2\n'
676
+ """
677
+ def __init__(self, example):
678
+ """
679
+ Swaps out the source for the sage_source of a doctest example.
680
+
681
+ INPUT:
682
+
683
+ - ``example`` -- a :class:`doctest.Example` instance
684
+
685
+ EXAMPLES::
686
+
687
+ sage: from sage.doctest.sources import FileDocTestSource
688
+ sage: from sage.doctest.control import DocTestDefaults
689
+ sage: filename = sage.doctest.forker.__file__
690
+ sage: FDS = FileDocTestSource(filename, DocTestDefaults())
691
+ sage: doctests, extras = FDS.create_doctests(globals())
692
+ sage: ex = doctests[0].examples[0]
693
+ sage: from sage.doctest.parsing import OriginalSource
694
+ sage: OriginalSource(ex)
695
+ <sage.doctest.parsing.OriginalSource object at ...>
696
+ """
697
+ self.example = example
698
+
699
+ def __enter__(self):
700
+ r"""
701
+ EXAMPLES::
702
+
703
+ sage: from sage.doctest.sources import FileDocTestSource
704
+ sage: from sage.doctest.control import DocTestDefaults
705
+ sage: filename = sage.doctest.forker.__file__
706
+ sage: FDS = FileDocTestSource(filename, DocTestDefaults())
707
+ sage: doctests, extras = FDS.create_doctests(globals())
708
+ sage: ex = doctests[0].examples[0]
709
+ sage: from sage.doctest.parsing import OriginalSource
710
+ sage: with OriginalSource(ex): # indirect doctest
711
+ ....: ex.source
712
+ 'doctest_var = 42; doctest_var^2\n'
713
+ """
714
+ if hasattr(self.example, 'sage_source'):
715
+ self.old_source, self.example.source = self.example.source, self.example.sage_source
716
+
717
+ def __exit__(self, *args):
718
+ r"""
719
+ EXAMPLES::
720
+
721
+ sage: from sage.doctest.sources import FileDocTestSource
722
+ sage: from sage.doctest.control import DocTestDefaults
723
+ sage: filename = sage.doctest.forker.__file__
724
+ sage: FDS = FileDocTestSource(filename, DocTestDefaults())
725
+ sage: doctests, extras = FDS.create_doctests(globals())
726
+ sage: ex = doctests[0].examples[0]
727
+ sage: from sage.doctest.parsing import OriginalSource
728
+ sage: with OriginalSource(ex): # indirect doctest
729
+ ....: ex.source
730
+ 'doctest_var = 42; doctest_var^2\n'
731
+ sage: ex.source # indirect doctest
732
+ 'doctest_var = Integer(42); doctest_var**Integer(2)\n'
733
+ """
734
+ if hasattr(self.example, 'sage_source'):
735
+ self.example.source = self.old_source
736
+
737
+
738
+ class SageDocTestParser(doctest.DocTestParser):
739
+ """
740
+ A version of the standard doctest parser which handles Sage's
741
+ custom options and tolerances in floating point arithmetic.
742
+ """
743
+
744
+ long: bool
745
+ file_optional_tags: set[str]
746
+ optional_tags: Union[bool, set[str]]
747
+ optional_only: bool
748
+ optionals: dict[str, int]
749
+ probed_tags: Union[bool, set[str]]
750
+
751
+ def __init__(self, optional_tags=(), long=False, *, probed_tags=(), file_optional_tags=()):
752
+ r"""
753
+ INPUT:
754
+
755
+ - ``optional_tags`` -- list or tuple of strings
756
+ - ``long`` -- boolean, whether to run doctests marked as taking a
757
+ long time
758
+ - ``probed_tags`` -- list or tuple of strings
759
+ - ``file_optional_tags`` -- an iterable of strings
760
+
761
+ EXAMPLES::
762
+
763
+ sage: from sage.doctest.parsing import SageDocTestParser
764
+ sage: DTP = SageDocTestParser(('sage','magma','guava'))
765
+ sage: ex = DTP.parse("sage: 2 + 2\n")[1]
766
+ sage: ex.sage_source
767
+ '2 + 2\n'
768
+ sage: ex = DTP.parse("sage: R.<x> = ZZ[]")[1]
769
+ sage: ex.source
770
+ "R = ZZ['x']; (x,) = R._first_ngens(1)\n"
771
+
772
+ TESTS::
773
+
774
+ sage: TestSuite(DTP).run()
775
+ """
776
+ self.long = long
777
+ self.optionals = defaultdict(int) # record skipped optional tests
778
+ if optional_tags is True: # run all optional tests
779
+ self.optional_tags = True
780
+ self.optional_only = False
781
+ else:
782
+ self.optional_tags = set(optional_tags)
783
+ if 'sage' in self.optional_tags:
784
+ self.optional_only = False
785
+ self.optional_tags.remove('sage')
786
+ else:
787
+ self.optional_only = True
788
+ if probed_tags is True:
789
+ self.probed_tags = True
790
+ else:
791
+ self.probed_tags = set(probed_tags)
792
+ self.file_optional_tags = set(file_optional_tags)
793
+
794
+ def __eq__(self, other):
795
+ """
796
+ Comparison.
797
+
798
+ EXAMPLES::
799
+
800
+ sage: from sage.doctest.parsing import SageDocTestParser
801
+ sage: DTP = SageDocTestParser(('sage','magma','guava'), True)
802
+ sage: DTP2 = SageDocTestParser(('sage','magma','guava'), False)
803
+ sage: DTP == DTP2
804
+ False
805
+ """
806
+ if not isinstance(other, SageDocTestParser):
807
+ return False
808
+ return self.__dict__ == other.__dict__
809
+
810
+ def __ne__(self, other):
811
+ """
812
+ Test for non-equality.
813
+
814
+ EXAMPLES::
815
+
816
+ sage: from sage.doctest.parsing import SageDocTestParser
817
+ sage: DTP = SageDocTestParser(('sage','magma','guava'), True)
818
+ sage: DTP2 = SageDocTestParser(('sage','magma','guava'), False)
819
+ sage: DTP != DTP2
820
+ True
821
+ """
822
+ return not (self == other)
823
+
824
+ def parse(self, string, *args):
825
+ r"""
826
+ A Sage specialization of :class:`doctest.DocTestParser`.
827
+
828
+ INPUT:
829
+
830
+ - ``string`` -- the string to parse
831
+ - ``name`` -- (optional) string giving the name identifying string,
832
+ to be used in error messages
833
+
834
+ OUTPUT:
835
+
836
+ - A list consisting of strings and :class:`doctest.Example`
837
+ instances. There will be at least one string between
838
+ successive examples (exactly one unless long or optional
839
+ tests are removed), and it will begin and end with a string.
840
+
841
+ EXAMPLES::
842
+
843
+ sage: from sage.doctest.parsing import SageDocTestParser
844
+ sage: DTP = SageDocTestParser(('sage','magma','guava'))
845
+ sage: example = 'Explanatory text::\n\n sage: E = magma("EllipticCurve([1, 1, 1, -10, -10])") # optional: magma\n\nLater text'
846
+ sage: parsed = DTP.parse(example)
847
+ sage: parsed[0]
848
+ 'Explanatory text::\n\n'
849
+ sage: parsed[1].sage_source
850
+ 'E = magma("EllipticCurve([1, 1, 1, -10, -10])") # optional: magma\n'
851
+ sage: parsed[2]
852
+ '\nLater text'
853
+
854
+ If the doctest parser is not created to accept a given
855
+ optional argument, the corresponding examples will just be
856
+ removed::
857
+
858
+ sage: DTP2 = SageDocTestParser(('sage',))
859
+ sage: parsed2 = DTP2.parse(example)
860
+ sage: parsed2
861
+ ['Explanatory text::\n\n', '\nLater text']
862
+
863
+ You can mark doctests as having a particular tolerance::
864
+
865
+ sage: example2 = 'sage: gamma(1.6) # tol 2.0e-11\n0.893515349287690'
866
+ sage: ex = DTP.parse(example2)[1]
867
+ sage: ex.sage_source
868
+ 'gamma(1.6) # tol 2.0e-11\n'
869
+ sage: ex.want
870
+ '0.893515349287690\n'
871
+ sage: type(ex.want)
872
+ <class 'sage.doctest.marked_output.MarkedOutput'>
873
+ sage: ex.want.tol # needs sage.rings.real_interval_field
874
+ 2.000000000000000000...?e-11
875
+
876
+ You can use continuation lines::
877
+
878
+ sage: s = "sage: for i in range(4):\n....: print(i)\n....:\n"
879
+ sage: ex = DTP2.parse(s)[1]
880
+ sage: ex.source
881
+ 'for i in range(Integer(4)):\n print(i)\n'
882
+
883
+ Sage currently accepts backslashes as indicating that the end
884
+ of the current line should be joined to the next line. This
885
+ feature allows for breaking large integers over multiple lines
886
+ but is not standard for Python doctesting. It's not
887
+ guaranteed to persist::
888
+
889
+ sage: n = 1234\
890
+ ....: 5678
891
+ sage: print(n)
892
+ 12345678
893
+ sage: type(n)
894
+ <class 'sage.rings.integer.Integer'>
895
+
896
+ It also works without the line continuation::
897
+
898
+ sage: m = 8765\
899
+ 4321
900
+ sage: print(m)
901
+ 87654321
902
+
903
+ Optional tags at the start of an example block persist to the end of
904
+ the block (delimited by a blank line)::
905
+
906
+ sage: # long time, needs sage.rings.number_field
907
+ sage: QQbar(I)^10000
908
+ 1
909
+ sage: QQbar(I)^10000 # not tested
910
+ I
911
+
912
+ sage: # needs sage.rings.finite_rings
913
+ sage: GF(7)
914
+ Finite Field of size 7
915
+ sage: GF(10)
916
+ Traceback (most recent call last):
917
+ ...
918
+ ValueError: the order of a finite field must be a prime power
919
+
920
+ Test that :issue:`26575` is resolved::
921
+
922
+ sage: example3 = 'sage: Zp(5,4,print_mode="digits")(5)\n...00010'
923
+ sage: parsed3 = DTP.parse(example3)
924
+ sage: dte = parsed3[1]
925
+ sage: dte.sage_source
926
+ 'Zp(5,4,print_mode="digits")(5)\n'
927
+ sage: dte.want
928
+ '...00010\n'
929
+
930
+ Style warnings::
931
+
932
+ sage: def parse(test_string):
933
+ ....: return [x if isinstance(x, str)
934
+ ....: else (getattr(x, 'warnings', None), x.sage_source, x.source)
935
+ ....: for x in DTP.parse(test_string)]
936
+
937
+ sage: parse('sage: 1 # optional guava mango\nsage: 2 # optional guava\nsage: 3 # optional guava\nsage: 4 # optional guava\nsage: 5 # optional guava\n\nsage: 11 # optional guava')
938
+ ['',
939
+ (["Consider using a block-scoped tag by inserting the line 'sage: # optional - guava' just before this line to avoid repeating the tag 5 times"],
940
+ '1 # optional guava mango\n',
941
+ 'None # virtual doctest'),
942
+ '',
943
+ (None, '2 # optional guava\n', 'Integer(2) # optional guava\n'),
944
+ '',
945
+ (None, '3 # optional guava\n', 'Integer(3) # optional guava\n'),
946
+ '',
947
+ (None, '4 # optional guava\n', 'Integer(4) # optional guava\n'),
948
+ '',
949
+ (None, '5 # optional guava\n', 'Integer(5) # optional guava\n'),
950
+ '\n',
951
+ (None, '11 # optional guava\n', 'Integer(11) # optional guava\n'),
952
+ '']
953
+
954
+ sage: parse('sage: 1 # optional guava\nsage: 2 # optional guava mango\nsage: 3 # optional guava\nsage: 4 # optional guava\nsage: 5 # optional guava\n')
955
+ ['',
956
+ (["Consider using a block-scoped tag by inserting the line 'sage: # optional - guava' just before this line to avoid repeating the tag 5 times"],
957
+ '1 # optional guava\n',
958
+ 'Integer(1) # optional guava\n'),
959
+ '',
960
+ '',
961
+ (None, '3 # optional guava\n', 'Integer(3) # optional guava\n'),
962
+ '',
963
+ (None, '4 # optional guava\n', 'Integer(4) # optional guava\n'),
964
+ '',
965
+ (None, '5 # optional guava\n', 'Integer(5) # optional guava\n'),
966
+ '']
967
+
968
+ sage: parse('sage: # optional mango\nsage: 1 # optional guava\nsage: 2 # optional guava mango\nsage: 3 # optional guava\nsage: 4 # optional guava\n sage: 5 # optional guava\n') # optional - guava mango
969
+ ['',
970
+ (["Consider updating this block-scoped tag to 'sage: # optional - guava mango' to avoid repeating the tag 5 times"],
971
+ '# optional mango\n',
972
+ 'None # virtual doctest'),
973
+ '',
974
+ '',
975
+ '',
976
+ '',
977
+ '',
978
+ '']
979
+
980
+ sage: parse('::\n\n sage: 1 # optional guava\n sage: 2 # optional guava mango\n sage: 3 # optional guava\n\n::\n\n sage: 4 # optional guava\n sage: 5 # optional guava\n')
981
+ ['::\n\n',
982
+ (None, '1 # optional guava\n', 'Integer(1) # optional guava\n'),
983
+ '',
984
+ '',
985
+ (None, '3 # optional guava\n', 'Integer(3) # optional guava\n'),
986
+ '\n::\n\n',
987
+ (None, '4 # optional guava\n', 'Integer(4) # optional guava\n'),
988
+ '',
989
+ (None, '5 # optional guava\n', 'Integer(5) # optional guava\n'),
990
+ '']
991
+
992
+ TESTS::
993
+
994
+ sage: # needs sage.combinat
995
+ sage: parse("::\n\n sage: # needs sage.combinat\n sage: from sage.geometry.polyhedron.combinatorial_polyhedron.conversions \\\n ....: import incidence_matrix_to_bit_rep_of_Vrep\n sage: P = polytopes.associahedron(['A',3])\n\n")
996
+ ['::\n\n',
997
+ '',
998
+ (None,
999
+ 'from sage.geometry.polyhedron.combinatorial_polyhedron.conversions import incidence_matrix_to_bit_rep_of_Vrep\n',
1000
+ 'from sage.geometry.polyhedron.combinatorial_polyhedron.conversions import incidence_matrix_to_bit_rep_of_Vrep\n'),
1001
+ '',
1002
+ (None,
1003
+ "P = polytopes.associahedron(['A',3])\n",
1004
+ "P = polytopes.associahedron(['A',Integer(3)])\n"),
1005
+ '\n']
1006
+
1007
+ sage: example4 = '::\n\n sage: C.minimum_distance(algorithm="guava") # optional - guava\n ...\n 24\n\n'
1008
+ sage: parsed4 = DTP.parse(example4)
1009
+ sage: dte = parsed4[1]
1010
+ sage: dte.sage_source
1011
+ 'C.minimum_distance(algorithm="guava") # optional - guava\n'
1012
+ sage: dte.want
1013
+ '...\n24\n'
1014
+ """
1015
+ # Regular expressions
1016
+ find_sage_prompt = re.compile(r"^(\s*)sage: ", re.M)
1017
+ find_sage_continuation = re.compile(r"^(\s*)\.\.\.\.:", re.M)
1018
+ find_python_continuation = re.compile(r"^(\s*)\.\.\.([^\.])", re.M)
1019
+ python_prompt = re.compile(r"^(\s*)>>>", re.M)
1020
+ backslash_replacer = re.compile(r"""(\s*)sage:(.*)\\\ *
1021
+ \ *((\.){4}:)?\ *""")
1022
+
1023
+ # The following are used to allow ... at the beginning of output
1024
+ ellipsis_tag = "<TEMP_ELLIPSIS_TAG>"
1025
+
1026
+ # Hack for non-standard backslash line escapes accepted by the current
1027
+ # doctest system.
1028
+ m = backslash_replacer.search(string)
1029
+ while m is not None:
1030
+ g = m.groups()
1031
+ string = string[:m.start()] + g[0] + "sage:" + g[1] + string[m.end():]
1032
+ m = backslash_replacer.search(string, m.start())
1033
+
1034
+ replace_ellipsis = not python_prompt.search(string)
1035
+ if replace_ellipsis:
1036
+ # There are no >>> prompts, so we can allow ... to begin the output
1037
+ # We do so by replacing ellipses with a special tag, then putting them back after parsing
1038
+ string = find_python_continuation.sub(r"\1" + ellipsis_tag + r"\2", string)
1039
+ string = find_sage_prompt.sub(r"\1>>> sage: ", string)
1040
+ string = find_sage_continuation.sub(r"\1...", string)
1041
+ res = doctest.DocTestParser.parse(self, string, *args)
1042
+ filtered = []
1043
+ persistent_optional_tags = self.file_optional_tags
1044
+ persistent_optional_tag_setter = None
1045
+ persistent_optional_tag_setter_index = None
1046
+ first_example_in_block = None
1047
+ first_example_in_block_index = None
1048
+ tag_count_within_block = defaultdict(lambda: 0)
1049
+
1050
+ def update_tag_counts(optional_tags):
1051
+ for tag in optional_tags:
1052
+ if tag not in persistent_optional_tags:
1053
+ tag_count_within_block[tag] += 1
1054
+ tag_count_within_block[''] += 1
1055
+
1056
+ def check_and_clear_tag_counts():
1057
+ if (num_examples := tag_count_within_block['']) >= 4:
1058
+ if overused_tags := {tag for tag, count in tag_count_within_block.items()
1059
+ if tag and count >= num_examples}:
1060
+ overused_tags.update(persistent_optional_tags)
1061
+ overused_tags.difference_update(self.file_optional_tags)
1062
+ suggested = unparse_optional_tags(overused_tags, prefix='sage: # ')
1063
+
1064
+ if persistent_optional_tag_setter:
1065
+ warning_example = persistent_optional_tag_setter
1066
+ index = persistent_optional_tag_setter_index
1067
+ warning = (f"Consider updating this block-scoped tag to '{suggested}' "
1068
+ f"to avoid repeating the tag {num_examples} times")
1069
+ else:
1070
+ warning_example = first_example_in_block
1071
+ index = first_example_in_block_index
1072
+ warning = (f"Consider using a block-scoped tag by "
1073
+ f"inserting the line '{suggested}' just before this line "
1074
+ f"to avoid repeating the tag {num_examples} times")
1075
+
1076
+ if not (index < len(filtered) and filtered[index] == warning_example):
1077
+ # The example to which we want to attach our warning is
1078
+ # not in ``filtered``. It is either the persistent tag line,
1079
+ # or the first example of the block and not run because of unmet tags,
1080
+ # or just a comment. Either way, we transform this example
1081
+ # to a virtual example and attach the warning to it.
1082
+ warning_example.sage_source = warning_example.source
1083
+ if warning_example.sage_source.startswith("sage: "):
1084
+ warning_example.sage_source = warning_example.source[6:]
1085
+ warning_example.source = 'None # virtual doctest'
1086
+ warning_example.want = ''
1087
+ filtered.insert(index, warning_example)
1088
+
1089
+ if not hasattr(warning_example, 'warnings'):
1090
+ warning_example.warnings = []
1091
+ warning_example.warnings.append(warning)
1092
+ tag_count_within_block.clear()
1093
+
1094
+ for item in res:
1095
+ if isinstance(item, doctest.Example):
1096
+ optional_tags_with_values, _, is_persistent = parse_optional_tags(item.source, return_string_sans_tags=True)
1097
+ optional_tags = set(optional_tags_with_values)
1098
+ if is_persistent:
1099
+ check_and_clear_tag_counts()
1100
+ persistent_optional_tags = optional_tags
1101
+ persistent_optional_tags.update(self.file_optional_tags)
1102
+ persistent_optional_tag_setter = first_example_in_block = item
1103
+ persistent_optional_tag_setter_index = len(filtered)
1104
+ first_example_in_block_index = None
1105
+ continue
1106
+
1107
+ if not first_example_in_block:
1108
+ first_example_in_block = item
1109
+ first_example_in_block_index = len(filtered)
1110
+ update_tag_counts(optional_tags)
1111
+ optional_tags.update(persistent_optional_tags)
1112
+ item.optional_tags = frozenset(optional_tags)
1113
+ item.probed_tags = set()
1114
+ if optional_tags:
1115
+ for tag in optional_tags:
1116
+ self.optionals[tag] += 1
1117
+ if (('not implemented' in optional_tags) or
1118
+ ('not tested' in optional_tags)):
1119
+ continue
1120
+
1121
+ if 'long time' in optional_tags:
1122
+ if self.long:
1123
+ optional_tags.remove('long time')
1124
+ else:
1125
+ continue
1126
+
1127
+ if self.optional_tags is not True:
1128
+ extra = {
1129
+ tag
1130
+ for tag in optional_tags
1131
+ if (
1132
+ tag not in self.optional_tags
1133
+ and tag not in available_software
1134
+ )
1135
+ }
1136
+ if extra and any(tag in ["bug"] for tag in extra):
1137
+ # Bug only occurs on a specific platform?
1138
+ bug_platform = optional_tags_with_values.get("bug")
1139
+ # System platform as either linux or macos
1140
+ system_platform = (
1141
+ platform.system().lower().replace("darwin", "macos")
1142
+ )
1143
+ if not bug_platform or bug_platform == system_platform:
1144
+ continue
1145
+ elif extra:
1146
+ if any(tag in external_software for tag in extra):
1147
+ # never probe "external" software
1148
+ continue
1149
+ if any(tag in ['webbrowser'] for tag in extra):
1150
+ # never probe
1151
+ continue
1152
+ if any(tag in ['got', 'expected', 'nameerror'] for tag in extra):
1153
+ # never probe special tags added by sage-fixdoctests
1154
+ continue
1155
+ if all(tag in persistent_optional_tags for tag in extra):
1156
+ # don't probe if test is only conditional
1157
+ # on file-level or block-level tags
1158
+ continue
1159
+ if self.probed_tags is True:
1160
+ item.probed_tags = extra
1161
+ elif all(tag in self.probed_tags for tag in extra):
1162
+ item.probed_tags = extra
1163
+ else:
1164
+ continue
1165
+ elif self.optional_only:
1166
+ self.optionals['sage'] += 1
1167
+ continue
1168
+
1169
+ if replace_ellipsis:
1170
+ item.want = item.want.replace(ellipsis_tag, "...")
1171
+ if item.exc_msg is not None:
1172
+ item.exc_msg = item.exc_msg.replace(ellipsis_tag, "...")
1173
+ item.want = parse_tolerance(item.source, item.want)
1174
+ if item.source.startswith("sage: "):
1175
+ item.sage_source = item.source[6:]
1176
+ if item.sage_source.lstrip().startswith('#'):
1177
+ continue
1178
+ item.source = preparse(item.sage_source)
1179
+ else:
1180
+ if '\n' in item:
1181
+ check_and_clear_tag_counts()
1182
+ persistent_optional_tags = self.file_optional_tags
1183
+ persistent_optional_tag_setter = first_example_in_block = None
1184
+ persistent_optional_tag_setter_index = first_example_in_block_index = None
1185
+ filtered.append(item)
1186
+
1187
+ check_and_clear_tag_counts()
1188
+
1189
+ return filtered
1190
+
1191
+
1192
+ class SageOutputChecker(doctest.OutputChecker):
1193
+ r"""
1194
+ A modification of the doctest OutputChecker that can check
1195
+ relative and absolute tolerance of answers.
1196
+
1197
+ EXAMPLES::
1198
+
1199
+ sage: from sage.doctest.parsing import SageOutputChecker, MarkedOutput, SageDocTestParser
1200
+ sage: import doctest
1201
+ sage: optflag = doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS
1202
+ sage: DTP = SageDocTestParser(('sage','magma','guava'))
1203
+ sage: OC = SageOutputChecker()
1204
+ sage: example2 = 'sage: gamma(1.6) # tol 2.0e-11\n0.893515349287690'
1205
+ sage: ex = DTP.parse(example2)[1]
1206
+ sage: ex.sage_source
1207
+ 'gamma(1.6) # tol 2.0e-11\n'
1208
+ sage: ex.want
1209
+ '0.893515349287690\n'
1210
+ sage: type(ex.want)
1211
+ <class 'sage.doctest.marked_output.MarkedOutput'>
1212
+
1213
+ sage: # needs sage.rings.real_interval_field
1214
+ sage: ex.want.tol
1215
+ 2.000000000000000000...?e-11
1216
+ sage: OC.check_output(ex.want, '0.893515349287690', optflag)
1217
+ True
1218
+ sage: OC.check_output(ex.want, '0.8935153492877', optflag)
1219
+ True
1220
+ sage: OC.check_output(ex.want, '0', optflag)
1221
+ False
1222
+ sage: OC.check_output(ex.want, 'x + 0.8935153492877', optflag)
1223
+ False
1224
+ """
1225
+ def human_readable_escape_sequences(self, string):
1226
+ r"""
1227
+ Make ANSI escape sequences human readable.
1228
+
1229
+ EXAMPLES::
1230
+
1231
+ sage: print('This is \x1b[1mbold\x1b[0m text')
1232
+ This is <CSI-1m>bold<CSI-0m> text
1233
+
1234
+ TESTS::
1235
+
1236
+ sage: from sage.doctest.parsing import SageOutputChecker
1237
+ sage: OC = SageOutputChecker()
1238
+ sage: teststr = '-'.join([
1239
+ ....: 'bold\x1b[1m',
1240
+ ....: 'red\x1b[31m',
1241
+ ....: 'oscmd\x1ba'])
1242
+ sage: OC.human_readable_escape_sequences(teststr)
1243
+ 'bold<CSI-1m>-red<CSI-31m>-oscmd<ESC-a>'
1244
+ """
1245
+ def human_readable(match):
1246
+ ansi_escape = match.group(1)
1247
+ assert len(ansi_escape) >= 2
1248
+ if len(ansi_escape) == 2:
1249
+ return '<ESC-' + ansi_escape[1] + '>'
1250
+ return '<CSI-' + ansi_escape.lstrip('\x1b[\x9b') + '>'
1251
+ return ansi_escape_sequence.subn(human_readable, string)[0]
1252
+
1253
+ def check_output(self, want, got, optionflags):
1254
+ r"""
1255
+ Check to see if the output matches the desired output.
1256
+
1257
+ If ``want`` is a :class:`MarkedOutput` instance, takes into account the desired tolerance.
1258
+
1259
+ INPUT:
1260
+
1261
+ - ``want`` -- string or :class:`MarkedOutput`
1262
+ - ``got`` -- string
1263
+ - ``optionflags`` -- integer; passed down to :class:`doctest.OutputChecker`
1264
+
1265
+ OUTPUT: boolean; whether ``got`` matches ``want`` up to the specified
1266
+ tolerance
1267
+
1268
+ EXAMPLES::
1269
+
1270
+ sage: from sage.doctest.parsing import MarkedOutput, SageOutputChecker
1271
+ sage: import doctest
1272
+ sage: optflag = doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS
1273
+ sage: rndstr = MarkedOutput("I'm wrong!").update(random=True)
1274
+ sage: tentol = MarkedOutput("10.0").update(tol=.1)
1275
+ sage: tenabs = MarkedOutput("10.0").update(abs_tol=.1)
1276
+ sage: tenrel = MarkedOutput("10.0").update(rel_tol=.1)
1277
+ sage: zerotol = MarkedOutput("0.0").update(tol=.1)
1278
+ sage: zeroabs = MarkedOutput("0.0").update(abs_tol=.1)
1279
+ sage: zerorel = MarkedOutput("0.0").update(rel_tol=.1)
1280
+ sage: zero = "0.0"
1281
+ sage: nf = "9.5"
1282
+ sage: ten = "10.05"
1283
+ sage: eps = "-0.05"
1284
+ sage: OC = SageOutputChecker()
1285
+
1286
+ ::
1287
+
1288
+ sage: OC.check_output(rndstr,nf,optflag)
1289
+ True
1290
+
1291
+ sage: # needs sage.rings.real_interval_field
1292
+ sage: OC.check_output(tentol,nf,optflag)
1293
+ True
1294
+ sage: OC.check_output(tentol,ten,optflag)
1295
+ True
1296
+ sage: OC.check_output(tentol,zero,optflag)
1297
+ False
1298
+
1299
+ sage: # needs sage.rings.real_interval_field
1300
+ sage: OC.check_output(tenabs,nf,optflag)
1301
+ False
1302
+ sage: OC.check_output(tenabs,ten,optflag)
1303
+ True
1304
+ sage: OC.check_output(tenabs,zero,optflag)
1305
+ False
1306
+
1307
+ sage: # needs sage.rings.real_interval_field
1308
+ sage: OC.check_output(tenrel,nf,optflag)
1309
+ True
1310
+ sage: OC.check_output(tenrel,ten,optflag)
1311
+ True
1312
+ sage: OC.check_output(tenrel,zero,optflag)
1313
+ False
1314
+
1315
+ sage: # needs sage.rings.real_interval_field
1316
+ sage: OC.check_output(zerotol,zero,optflag)
1317
+ True
1318
+ sage: OC.check_output(zerotol,eps,optflag)
1319
+ True
1320
+ sage: OC.check_output(zerotol,ten,optflag)
1321
+ False
1322
+
1323
+ sage: # needs sage.rings.real_interval_field
1324
+ sage: OC.check_output(zeroabs,zero,optflag)
1325
+ True
1326
+ sage: OC.check_output(zeroabs,eps,optflag)
1327
+ True
1328
+ sage: OC.check_output(zeroabs,ten,optflag)
1329
+ False
1330
+
1331
+ sage: # needs sage.rings.real_interval_field
1332
+ sage: OC.check_output(zerorel,zero,optflag)
1333
+ True
1334
+ sage: OC.check_output(zerorel,eps,optflag)
1335
+ False
1336
+ sage: OC.check_output(zerorel,ten,optflag)
1337
+ False
1338
+
1339
+ More explicit tolerance checks::
1340
+
1341
+ sage: # needs sage.rings.real_interval_field
1342
+ sage: _ = x # rel tol 1e10 # needs sage.symbolic
1343
+ sage: raise RuntimeError # rel tol 1e10
1344
+ Traceback (most recent call last):
1345
+ ...
1346
+ RuntimeError
1347
+ sage: 1 # abs tol 2
1348
+ -0.5
1349
+ sage: print("0.9999") # rel tol 1e-4
1350
+ 1.0
1351
+ sage: print("1.00001") # abs tol 1e-5
1352
+ 1.0
1353
+ sage: 0 # rel tol 1
1354
+ 1
1355
+
1356
+ Abs tol checks over the complex domain::
1357
+
1358
+ sage: # needs sage.rings.real_interval_field sage.symbolic
1359
+ sage: [1, -1.3, -1.5 + 0.1*I, 0.5 - 0.1*I, -1.5*I] # abs tol 1.0
1360
+ [1, -1, -1, 1, -I]
1361
+
1362
+ Spaces before numbers or between the sign and number are ignored::
1363
+
1364
+ sage: # needs sage.rings.real_interval_field
1365
+ sage: print("[ - 1, 2]") # abs tol 1e-10
1366
+ [-1,2]
1367
+
1368
+ Tolerance on Python 3 for string results with unicode prefix::
1369
+
1370
+ sage: a = 'Cyrano'; a
1371
+ 'Cyrano'
1372
+ sage: b = ['Fermat', 'Euler']; b
1373
+ ['Fermat', 'Euler']
1374
+ sage: c = 'you'; c
1375
+ 'you'
1376
+
1377
+ This illustrates that :issue:`33588` is fixed::
1378
+
1379
+ sage: from sage.doctest.parsing import SageOutputChecker, SageDocTestParser
1380
+ sage: import doctest
1381
+ sage: optflag = doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS
1382
+ sage: DTP = SageDocTestParser(('sage','magma','guava'))
1383
+ sage: OC = SageOutputChecker()
1384
+ sage: example = "sage: 1.3090169943749475 # tol 1e-8\n1.3090169943749475"
1385
+ sage: ex = DTP.parse(example)[1]
1386
+ sage: OC.check_output(ex.want, '1.3090169943749475', optflag)
1387
+ True
1388
+ sage: OC.check_output(ex.want, 'ANYTHING1.3090169943749475', optflag)
1389
+ False
1390
+ sage: OC.check_output(ex.want, 'Long-step dual simplex will be used\n1.3090169943749475', optflag)
1391
+ True
1392
+ """
1393
+ got = self.human_readable_escape_sequences(got)
1394
+ try:
1395
+ if isinstance(want, MarkedOutput):
1396
+ if want.random:
1397
+ return True
1398
+ elif want.tol or want.rel_tol:
1399
+ want, got = check_tolerance_real_domain(want, got)
1400
+ elif want.abs_tol:
1401
+ want, got = check_tolerance_complex_domain(want, got)
1402
+ except ToleranceExceededError:
1403
+ return False
1404
+
1405
+ if doctest.OutputChecker.check_output(self, want, got, optionflags):
1406
+ return True
1407
+ else:
1408
+ # Last resort: try to fix-up the got string removing few typical warnings
1409
+ did_fixup, want, got = self.do_fixup(want, got)
1410
+ if did_fixup:
1411
+ return doctest.OutputChecker.check_output(self, want, got, optionflags)
1412
+ else:
1413
+ return False
1414
+
1415
+ def do_fixup(self, want, got):
1416
+ r"""
1417
+ Perform few changes to the strings ``want`` and ``got``.
1418
+
1419
+ For example, remove warnings to be ignored.
1420
+
1421
+ INPUT:
1422
+
1423
+ - ``want`` -- string or :class:`MarkedOutput`
1424
+ - ``got`` -- string
1425
+
1426
+ OUTPUT: a tuple:
1427
+
1428
+ - boolean, ``True`` when some fixup were performed and ``False`` otherwise
1429
+ - string, edited wanted string
1430
+ - string, edited got string
1431
+
1432
+ .. NOTE::
1433
+
1434
+ Currently, the code only possibly changes the string ``got``
1435
+ while keeping ``want`` invariant. We keep open the possibility
1436
+ of adding a regular expression which would also change the
1437
+ ``want`` string. This is why ``want`` is an input and an output
1438
+ of the method even if currently kept invariant.
1439
+
1440
+ EXAMPLES::
1441
+
1442
+ sage: from sage.doctest.parsing import SageOutputChecker
1443
+ sage: OC = SageOutputChecker()
1444
+ sage: OC.do_fixup('1.3090169943749475','1.3090169943749475')
1445
+ (False, '1.3090169943749475', '1.3090169943749475')
1446
+ sage: OC.do_fixup('1.3090169943749475','ANYTHING1.3090169943749475')
1447
+ (False, '1.3090169943749475', 'ANYTHING1.3090169943749475')
1448
+ sage: OC.do_fixup('1.3090169943749475','Long-step dual simplex will be used\n1.3090169943749475')
1449
+ (True, '1.3090169943749475', '\n1.3090169943749475')
1450
+
1451
+ When ``want`` is an instance of class :class:`MarkedOutput`::
1452
+
1453
+ sage: from sage.doctest.parsing import SageOutputChecker, SageDocTestParser
1454
+ sage: import doctest
1455
+ sage: optflag = doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS
1456
+ sage: DTP = SageDocTestParser(('sage','magma','guava'))
1457
+ sage: OC = SageOutputChecker()
1458
+ sage: example = "sage: 1.3090169943749475\n1.3090169943749475"
1459
+ sage: ex = DTP.parse(example)[1]
1460
+ sage: ex.want
1461
+ '1.3090169943749475\n'
1462
+ sage: OC.do_fixup(ex.want,'1.3090169943749475')
1463
+ (False, '1.3090169943749475\n', '1.3090169943749475')
1464
+ sage: OC.do_fixup(ex.want,'ANYTHING1.3090169943749475')
1465
+ (False, '1.3090169943749475\n', 'ANYTHING1.3090169943749475')
1466
+ sage: OC.do_fixup(ex.want,'Long-step dual simplex will be used\n1.3090169943749475')
1467
+ (True, '1.3090169943749475\n', '\n1.3090169943749475')
1468
+ """
1469
+ did_fixup = False
1470
+
1471
+ # The conditions in the below `if` are simple fast test on the expected
1472
+ # and/or actual output to determine if a fixup should be applied.
1473
+
1474
+ if "Long-step" in got:
1475
+ # Version 4.65 of glpk prints the warning "Long-step dual
1476
+ # simplex will be used" frequently. When Sage uses a system
1477
+ # installation of glpk which has not been patched, we need to
1478
+ # ignore that message. See :issue:`29317`.
1479
+ glpk_simplex_warning_regex = re.compile(r'(Long-step dual simplex will be used)')
1480
+ got = glpk_simplex_warning_regex.sub('', got)
1481
+ did_fixup = True
1482
+
1483
+ if "chained fixups" in got:
1484
+ # :issue:`34533` -- suppress warning on OS X 12.6 about chained fixups
1485
+ chained_fixup_warning_regex = re.compile(r'ld: warning: -undefined dynamic_lookup may not work with chained fixups')
1486
+ got = chained_fixup_warning_regex.sub('', got)
1487
+ did_fixup = True
1488
+
1489
+ if "newer macOS version" in got:
1490
+ # :issue:`34741` -- suppress warning arising after
1491
+ # upgrading from macOS 12.X to 13.X.
1492
+ newer_macOS_version_regex = re.compile(r'.*dylib \(.*\) was built for newer macOS version \(.*\) than being linked \(.*\)')
1493
+ got = newer_macOS_version_regex.sub('', got)
1494
+ did_fixup = True
1495
+
1496
+ if "insufficient permissions" in got:
1497
+ sympow_cache_warning_regex = re.compile(r'\*\*WARNING\*\* /var/cache/sympow/datafiles/le64 yields insufficient permissions')
1498
+ got = sympow_cache_warning_regex.sub('', got)
1499
+ did_fixup = True
1500
+
1501
+ if "dylib" in got:
1502
+ # :issue:`31204` -- suppress warning about ld and OS version for
1503
+ # dylib files.
1504
+ ld_warning_regex = re.compile(r'^.*dylib.*was built for newer macOS version.*than being linked.*')
1505
+ got = ld_warning_regex.sub('', got)
1506
+ did_fixup = True
1507
+
1508
+ if "pie being ignored" in got:
1509
+ # :issue:`30845` -- suppress warning on conda about ld
1510
+ ld_pie_warning_regex = re.compile(r'ld: warning: -pie being ignored. It is only used when linking a main executable')
1511
+ got = ld_pie_warning_regex.sub('', got)
1512
+ did_fixup = True
1513
+
1514
+ if "R[write to console]" in got:
1515
+ # Supress R warnings
1516
+ r_warning_regex = re.compile(r'R\[write to console\]:.*')
1517
+ got = r_warning_regex.sub('', got)
1518
+ did_fixup = True
1519
+
1520
+ if "Overriding pythran description" in got:
1521
+ # Some signatures changed in numpy-1.25.x that may yet be
1522
+ # reverted, but which pythran would otherwise warn about.
1523
+ # Pythran has a special case for numpy.random that hides
1524
+ # the warning -- I guess until we know if the changes will
1525
+ # be reverted -- but only in v0.14.0 of pythran. Ignoring
1526
+ # This warning allows us to support older pythran with e.g.
1527
+ # numpy-1.25.2.
1528
+ pythran_numpy_warning_regex = re.compile(r'WARNING: Overriding pythran description with argspec information for: numpy\.random\.[a-z_]+')
1529
+ got = pythran_numpy_warning_regex.sub('', got)
1530
+ did_fixup = True
1531
+
1532
+ if "ld_classic is deprecated" in got:
1533
+ # New warnings as of Oct '24, Xcode 16.
1534
+ ld_warn_regex = re.compile("ld: warning: -ld_classic is deprecated and will be removed in a future release")
1535
+ got = ld_warn_regex.sub('', got)
1536
+ did_fixup = True
1537
+
1538
+ if "duplicate libraries" in got:
1539
+ # New warnings as of Sept '23, OS X 13.6, new command-line
1540
+ # tools. In particular, these seem to come from ld in
1541
+ # Xcode 15.
1542
+ dup_lib_regex = re.compile("ld: warning: ignoring duplicate libraries: .*")
1543
+ got = dup_lib_regex.sub('', got)
1544
+ did_fixup = True
1545
+
1546
+ return did_fixup, want, got
1547
+
1548
+ def output_difference(self, example, got, optionflags):
1549
+ r"""
1550
+ Report on the differences between the desired result and what
1551
+ was actually obtained.
1552
+
1553
+ If ``want`` is a :class:`MarkedOutput` instance, takes into account the desired tolerance.
1554
+
1555
+ INPUT:
1556
+
1557
+ - ``example`` -- a :class:`doctest.Example` instance
1558
+ - ``got`` -- string
1559
+ - ``optionflags`` -- integer; passed down to :class:`doctest.OutputChecker`
1560
+
1561
+ OUTPUT: string, describing how ``got`` fails to match ``example.want``
1562
+
1563
+ EXAMPLES::
1564
+
1565
+ sage: from sage.doctest.parsing import MarkedOutput, SageOutputChecker
1566
+ sage: import doctest
1567
+ sage: optflag = doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS
1568
+ sage: tentol = doctest.Example('',MarkedOutput("10.0\n").update(tol=.1))
1569
+ sage: tenabs = doctest.Example('',MarkedOutput("10.0\n").update(abs_tol=.1))
1570
+ sage: tenrel = doctest.Example('',MarkedOutput("10.0\n").update(rel_tol=.1))
1571
+ sage: zerotol = doctest.Example('',MarkedOutput("0.0\n").update(tol=.1))
1572
+ sage: zeroabs = doctest.Example('',MarkedOutput("0.0\n").update(abs_tol=.1))
1573
+ sage: zerorel = doctest.Example('',MarkedOutput("0.0\n").update(rel_tol=.1))
1574
+ sage: tlist = doctest.Example('',MarkedOutput("[10.0, 10.0, 10.0, 10.0, 10.0, 10.0]\n").update(abs_tol=0.987))
1575
+ sage: zero = "0.0"
1576
+ sage: nf = "9.5"
1577
+ sage: ten = "10.05"
1578
+ sage: eps = "-0.05"
1579
+ sage: L = "[9.9, 8.7, 10.3, 11.2, 10.8, 10.0]"
1580
+ sage: OC = SageOutputChecker()
1581
+
1582
+ ::
1583
+
1584
+ sage: # needs sage.rings.real_interval_field
1585
+ sage: print(OC.output_difference(tenabs,nf,optflag))
1586
+ Expected:
1587
+ 10.0
1588
+ Got:
1589
+ 9.5
1590
+ Tolerance exceeded:
1591
+ 10.0 vs 9.5, tolerance 5e-1 > 1e-1
1592
+ sage: print(OC.output_difference(tentol,zero,optflag))
1593
+ Expected:
1594
+ 10.0
1595
+ Got:
1596
+ 0.0
1597
+ Tolerance exceeded:
1598
+ 10.0 vs 0.0, tolerance 1e0 > 1e-1
1599
+ sage: print(OC.output_difference(tentol,eps,optflag))
1600
+ Expected:
1601
+ 10.0
1602
+ Got:
1603
+ -0.05
1604
+ Tolerance exceeded:
1605
+ 10.0 vs -0.05, tolerance 2e0 > 1e-1
1606
+ sage: print(OC.output_difference(tlist,L,optflag))
1607
+ Expected:
1608
+ [10.0, 10.0, 10.0, 10.0, 10.0, 10.0]
1609
+ Got:
1610
+ [9.9, 8.7, 10.3, 11.2, 10.8, 10.0]
1611
+ Tolerance exceeded in 2 of 6:
1612
+ 10.0 vs 8.7, tolerance 2e0 > 9.87e-1
1613
+ 10.0 vs 11.2, tolerance 2e0 > 9.87e-1
1614
+
1615
+ TESTS::
1616
+
1617
+ sage: # needs sage.rings.real_interval_field
1618
+ sage: print(OC.output_difference(tenabs,zero,optflag))
1619
+ Expected:
1620
+ 10.0
1621
+ Got:
1622
+ 0.0
1623
+ Tolerance exceeded:
1624
+ 10.0 vs 0.0, tolerance 1e1 > 1e-1
1625
+ sage: print(OC.output_difference(tenrel,zero,optflag))
1626
+ Expected:
1627
+ 10.0
1628
+ Got:
1629
+ 0.0
1630
+ Tolerance exceeded:
1631
+ 10.0 vs 0.0, tolerance 1e0 > 1e-1
1632
+ sage: print(OC.output_difference(tenrel,eps,optflag))
1633
+ Expected:
1634
+ 10.0
1635
+ Got:
1636
+ -0.05
1637
+ Tolerance exceeded:
1638
+ 10.0 vs -0.05, tolerance 2e0 > 1e-1
1639
+ sage: print(OC.output_difference(zerotol,ten,optflag))
1640
+ Expected:
1641
+ 0.0
1642
+ Got:
1643
+ 10.05
1644
+ Tolerance exceeded:
1645
+ 0.0 vs 10.05, tolerance 2e1 > 1e-1
1646
+ sage: print(OC.output_difference(zeroabs,ten,optflag))
1647
+ Expected:
1648
+ 0.0
1649
+ Got:
1650
+ 10.05
1651
+ Tolerance exceeded:
1652
+ 0.0 vs 10.05, tolerance 2e1 > 1e-1
1653
+ sage: print(OC.output_difference(zerorel,eps,optflag))
1654
+ Expected:
1655
+ 0.0
1656
+ Got:
1657
+ -0.05
1658
+ Tolerance exceeded:
1659
+ 0.0 vs -0.05, tolerance +infinity > 1e-1
1660
+ sage: print(OC.output_difference(zerorel,ten,optflag))
1661
+ Expected:
1662
+ 0.0
1663
+ Got:
1664
+ 10.05
1665
+ Tolerance exceeded:
1666
+ 0.0 vs 10.05, tolerance +infinity > 1e-1
1667
+ """
1668
+ got = self.human_readable_escape_sequences(got)
1669
+ want = example.want
1670
+ diff = doctest.OutputChecker.output_difference(self, example, got, optionflags)
1671
+ if isinstance(want, MarkedOutput) and (want.tol or want.abs_tol or want.rel_tol):
1672
+ if diff[-1] != "\n":
1673
+ diff += "\n"
1674
+ want_str = [g[0] for g in float_regex.findall(want)]
1675
+ got_str = [g[0] for g in float_regex.findall(got)]
1676
+ if len(want_str) == len(got_str):
1677
+ failures = []
1678
+
1679
+ def fail(x, y, actual, desired):
1680
+ failstr = " {} vs {}, tolerance {} > {}".format(x, y,
1681
+ RIFtol(actual).upper().str(digits=1, no_sci=False),
1682
+ RIFtol(desired).center().str(digits=15, skip_zeroes=True, no_sci=False)
1683
+ )
1684
+ failures.append(failstr)
1685
+
1686
+ for wstr, gstr in zip(want_str, got_str):
1687
+ w = RIFtol(wstr)
1688
+ g = RIFtol(gstr)
1689
+ if not g.overlaps(add_tolerance(w, want)):
1690
+ if want.tol:
1691
+ if not w:
1692
+ fail(wstr, gstr, abs(g), want.tol)
1693
+ else:
1694
+ fail(wstr, gstr, abs(1 - g / w), want.tol)
1695
+ elif want.abs_tol:
1696
+ fail(wstr, gstr, abs(g - w), want.abs_tol)
1697
+ else:
1698
+ fail(wstr, gstr, abs(1 - g / w), want.rel_tol)
1699
+
1700
+ if failures:
1701
+ if len(want_str) == 1:
1702
+ diff += "Tolerance exceeded:\n"
1703
+ else:
1704
+ diff += "Tolerance exceeded in %s of %s:\n" % (len(failures), len(want_str))
1705
+ diff += "\n".join(failures) + "\n"
1706
+ elif "..." in want:
1707
+ diff += "Note: combining tolerance (# tol) with ellipsis (...) is not supported\n"
1708
+ return diff