passagemath-repl 10.5.1__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.
- passagemath_repl-10.5.1.data/scripts/sage-cachegrind +25 -0
- passagemath_repl-10.5.1.data/scripts/sage-callgrind +16 -0
- passagemath_repl-10.5.1.data/scripts/sage-cleaner +230 -0
- passagemath_repl-10.5.1.data/scripts/sage-coverage +327 -0
- passagemath_repl-10.5.1.data/scripts/sage-eval +14 -0
- passagemath_repl-10.5.1.data/scripts/sage-fixdoctests +710 -0
- passagemath_repl-10.5.1.data/scripts/sage-inline-fortran +12 -0
- passagemath_repl-10.5.1.data/scripts/sage-ipynb2rst +50 -0
- passagemath_repl-10.5.1.data/scripts/sage-ipython +16 -0
- passagemath_repl-10.5.1.data/scripts/sage-massif +25 -0
- passagemath_repl-10.5.1.data/scripts/sage-notebook +267 -0
- passagemath_repl-10.5.1.data/scripts/sage-omega +25 -0
- passagemath_repl-10.5.1.data/scripts/sage-preparse +302 -0
- passagemath_repl-10.5.1.data/scripts/sage-run +27 -0
- passagemath_repl-10.5.1.data/scripts/sage-run-cython +10 -0
- passagemath_repl-10.5.1.data/scripts/sage-runtests +9 -0
- passagemath_repl-10.5.1.data/scripts/sage-startuptime.py +163 -0
- passagemath_repl-10.5.1.data/scripts/sage-valgrind +34 -0
- passagemath_repl-10.5.1.dist-info/METADATA +77 -0
- passagemath_repl-10.5.1.dist-info/RECORD +162 -0
- passagemath_repl-10.5.1.dist-info/WHEEL +5 -0
- passagemath_repl-10.5.1.dist-info/top_level.txt +1 -0
- sage/all__sagemath_repl.py +119 -0
- sage/doctest/__init__.py +4 -0
- sage/doctest/__main__.py +236 -0
- sage/doctest/all.py +4 -0
- sage/doctest/check_tolerance.py +261 -0
- sage/doctest/control.py +1727 -0
- sage/doctest/external.py +534 -0
- sage/doctest/fixtures.py +383 -0
- sage/doctest/forker.py +2665 -0
- sage/doctest/marked_output.py +102 -0
- sage/doctest/parsing.py +1708 -0
- sage/doctest/parsing_test.py +79 -0
- sage/doctest/reporting.py +733 -0
- sage/doctest/rif_tol.py +124 -0
- sage/doctest/sources.py +1657 -0
- sage/doctest/test.py +584 -0
- sage/doctest/tests/1second.rst +4 -0
- sage/doctest/tests/99seconds.rst +4 -0
- sage/doctest/tests/abort.rst +5 -0
- sage/doctest/tests/atexit.rst +7 -0
- sage/doctest/tests/fail_and_die.rst +6 -0
- sage/doctest/tests/initial.rst +15 -0
- sage/doctest/tests/interrupt.rst +7 -0
- sage/doctest/tests/interrupt_diehard.rst +14 -0
- sage/doctest/tests/keyboardinterrupt.rst +11 -0
- sage/doctest/tests/longtime.rst +5 -0
- sage/doctest/tests/nodoctest +5 -0
- sage/doctest/tests/random_seed.rst +4 -0
- sage/doctest/tests/show_skipped.rst +18 -0
- sage/doctest/tests/sig_on.rst +9 -0
- sage/doctest/tests/simple_failure.rst +8 -0
- sage/doctest/tests/sleep_and_raise.rst +106 -0
- sage/doctest/tests/tolerance.rst +31 -0
- sage/doctest/util.py +750 -0
- sage/interfaces/cleaner.py +48 -0
- sage/interfaces/quit.py +163 -0
- sage/misc/all__sagemath_repl.py +51 -0
- sage/misc/banner.py +235 -0
- sage/misc/benchmark.py +221 -0
- sage/misc/classgraph.py +134 -0
- sage/misc/copying.py +22 -0
- sage/misc/cython.py +694 -0
- sage/misc/dev_tools.py +745 -0
- sage/misc/edit_module.py +304 -0
- sage/misc/explain_pickle.py +3079 -0
- sage/misc/gperftools.py +361 -0
- sage/misc/inline_fortran.py +212 -0
- sage/misc/messaging.py +86 -0
- sage/misc/pager.py +21 -0
- sage/misc/profiler.py +179 -0
- sage/misc/python.py +70 -0
- sage/misc/remote_file.py +53 -0
- sage/misc/sage_eval.py +249 -0
- sage/misc/sage_input.py +3621 -0
- sage/misc/sagedoc.py +1742 -0
- sage/misc/sh.py +38 -0
- sage/misc/trace.py +90 -0
- sage/repl/__init__.py +16 -0
- sage/repl/all.py +15 -0
- sage/repl/attach.py +625 -0
- sage/repl/configuration.py +186 -0
- sage/repl/display/__init__.py +1 -0
- sage/repl/display/fancy_repr.py +354 -0
- sage/repl/display/formatter.py +318 -0
- sage/repl/display/jsmol_iframe.py +290 -0
- sage/repl/display/pretty_print.py +153 -0
- sage/repl/display/util.py +163 -0
- sage/repl/image.py +302 -0
- sage/repl/inputhook.py +91 -0
- sage/repl/interface_magic.py +298 -0
- sage/repl/interpreter.py +854 -0
- sage/repl/ipython_extension.py +593 -0
- sage/repl/ipython_kernel/__init__.py +1 -0
- sage/repl/ipython_kernel/__main__.py +4 -0
- sage/repl/ipython_kernel/all_jupyter.py +10 -0
- sage/repl/ipython_kernel/install.py +301 -0
- sage/repl/ipython_kernel/interact.py +278 -0
- sage/repl/ipython_kernel/kernel.py +217 -0
- sage/repl/ipython_kernel/widgets.py +466 -0
- sage/repl/ipython_kernel/widgets_sagenb.py +587 -0
- sage/repl/ipython_tests.py +163 -0
- sage/repl/load.py +326 -0
- sage/repl/preparse.py +2218 -0
- sage/repl/prompts.py +90 -0
- sage/repl/rich_output/__init__.py +4 -0
- sage/repl/rich_output/backend_base.py +648 -0
- sage/repl/rich_output/backend_doctest.py +316 -0
- sage/repl/rich_output/backend_emacs.py +151 -0
- sage/repl/rich_output/backend_ipython.py +596 -0
- sage/repl/rich_output/buffer.py +311 -0
- sage/repl/rich_output/display_manager.py +829 -0
- sage/repl/rich_output/example.avi +0 -0
- sage/repl/rich_output/example.canvas3d +1 -0
- sage/repl/rich_output/example.dvi +0 -0
- sage/repl/rich_output/example.flv +0 -0
- sage/repl/rich_output/example.gif +0 -0
- sage/repl/rich_output/example.jpg +0 -0
- sage/repl/rich_output/example.mkv +0 -0
- sage/repl/rich_output/example.mov +0 -0
- sage/repl/rich_output/example.mp4 +0 -0
- sage/repl/rich_output/example.ogv +0 -0
- sage/repl/rich_output/example.pdf +0 -0
- sage/repl/rich_output/example.png +0 -0
- sage/repl/rich_output/example.svg +54 -0
- sage/repl/rich_output/example.webm +0 -0
- sage/repl/rich_output/example.wmv +0 -0
- sage/repl/rich_output/example_jmol.spt.zip +0 -0
- sage/repl/rich_output/example_wavefront_scene.mtl +7 -0
- sage/repl/rich_output/example_wavefront_scene.obj +17 -0
- sage/repl/rich_output/output_basic.py +391 -0
- sage/repl/rich_output/output_browser.py +103 -0
- sage/repl/rich_output/output_catalog.py +54 -0
- sage/repl/rich_output/output_graphics.py +320 -0
- sage/repl/rich_output/output_graphics3d.py +345 -0
- sage/repl/rich_output/output_video.py +231 -0
- sage/repl/rich_output/preferences.py +432 -0
- sage/repl/rich_output/pretty_print.py +339 -0
- sage/repl/rich_output/test_backend.py +201 -0
- sage/repl/user_globals.py +214 -0
- sage/tests/all.py +0 -0
- sage/tests/all__sagemath_repl.py +3 -0
- sage/tests/article_heuberger_krenn_kropf_fsm-in-sage.py +630 -0
- sage/tests/arxiv_0812_2725.py +351 -0
- sage/tests/benchmark.py +1925 -0
- sage/tests/book_schilling_zabrocki_kschur_primer.py +795 -0
- sage/tests/book_stein_ent.py +651 -0
- sage/tests/book_stein_modform.py +558 -0
- sage/tests/cmdline.py +796 -0
- sage/tests/combinatorial_hopf_algebras.py +52 -0
- sage/tests/finite_poset.py +623 -0
- sage/tests/functools_partial_src.py +27 -0
- sage/tests/gosper-sum.py +218 -0
- sage/tests/lazy_imports.py +28 -0
- sage/tests/modular_group_cohomology.py +80 -0
- sage/tests/numpy.py +21 -0
- sage/tests/parigp.py +76 -0
- sage/tests/startup.py +27 -0
- sage/tests/symbolic-series.py +76 -0
- sage/tests/sympy.py +16 -0
- sage/tests/test_deprecation.py +31 -0
sage/doctest/parsing.py
ADDED
@@ -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 = set()
|
1129
|
+
for tag in optional_tags:
|
1130
|
+
if tag not in self.optional_tags:
|
1131
|
+
if tag.startswith('!'):
|
1132
|
+
if tag[1:] in available_software:
|
1133
|
+
extra.add(tag)
|
1134
|
+
elif tag not in available_software:
|
1135
|
+
extra.add(tag)
|
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
|