Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/doctest/parsing.py
8817 views
1
## -*- encoding: utf-8 -*-
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
12
#*****************************************************************************
13
# Copyright (C) 2012 David Roe <[email protected]>
14
# Robert Bradshaw <[email protected]>
15
# William Stein <[email protected]>
16
#
17
# Distributed under the terms of the GNU General Public License (GPL)
18
# as published by the Free Software Foundation; either version 2 of
19
# the License, or (at your option) any later version.
20
# http://www.gnu.org/licenses/
21
#*****************************************************************************
22
23
24
import re, sys
25
import doctest
26
import collections
27
from sage.misc.preparser import preparse, strip_string_literals
28
29
float_regex = re.compile('([+-]?((\d*\.?\d+)|(\d+\.?))([eE][+-]?\d+)?)')
30
optional_regex = re.compile(r'(long time|not implemented|not tested|known bug)|([^ a-z]\s*optional\s*[:-]*((\s|\w)*))')
31
find_sage_prompt = re.compile(r"^(\s*)sage: ", re.M)
32
find_sage_continuation = re.compile(r"^(\s*)\.\.\.\.:", re.M)
33
random_marker = re.compile('.*random', re.I)
34
tolerance_pattern = re.compile(r'\b((?:abs(?:olute)?)|(?:rel(?:ative)?))? *?tol(?:erance)?\b( +[0-9.e+-]+)?')
35
backslash_replacer = re.compile(r"""(\s*)sage:(.*)\\\ *
36
\ *(((\.){4}:)|((\.){3}))?\ *""")
37
38
39
# This is the correct pattern to match ISO/IEC 6429 ANSI escape sequences:
40
#
41
#ansi_escape_sequence = re.compile(r'(\x1b[@-Z\\-~]|\x1b\[.*?[@-~]|\x9b.*?[@-~])')
42
#
43
# Unfortunately, we cannot use this, since the \x9b might be part of
44
# a UTF-8 character. Once we have a unicode-aware doctest framework, we
45
# should use the correct pattern including \x9b. For now, we use this
46
# form without \x9b:
47
ansi_escape_sequence = re.compile(r'(\x1b[@-Z\\-~]|\x1b\[.*?[@-~])')
48
49
50
def parse_optional_tags(string):
51
"""
52
Returns a set consisting of the optional tags from the following
53
set that occur in a comment on the first line of the input string.
54
55
- 'long time'
56
- 'not implemented'
57
- 'not tested'
58
- 'known bug'
59
- 'optional: PKG_NAME' -- the set will just contain 'PKG_NAME'
60
61
EXAMPLES::
62
63
sage: from sage.doctest.parsing import parse_optional_tags
64
sage: parse_optional_tags("sage: magma('2 + 2')# optional: magma")
65
set(['magma'])
66
sage: parse_optional_tags("sage: #optional -- mypkg")
67
set(['mypkg'])
68
sage: parse_optional_tags("sage: print(1) # parentheses are optional here")
69
set([])
70
sage: parse_optional_tags("sage: print(1) # optional")
71
set([''])
72
sage: sorted(list(parse_optional_tags("sage: #optional -- foo bar, baz")))
73
['bar', 'foo']
74
sage: sorted(list(parse_optional_tags(" sage: factor(10^(10^10) + 1) # LoNg TiME, NoT TeSTED; OptioNAL -- P4cka9e")))
75
['long time', 'not tested', 'p4cka9e']
76
sage: parse_optional_tags(" sage: raise RuntimeError # known bug")
77
set(['bug'])
78
sage: sorted(list(parse_optional_tags(" sage: determine_meaning_of_life() # long time, not implemented")))
79
['long time', 'not implemented']
80
81
We don't parse inside strings::
82
83
sage: parse_optional_tags(" sage: print ' # long time'")
84
set([])
85
sage: parse_optional_tags(" sage: print ' # long time' # not tested")
86
set(['not tested'])
87
88
UTF-8 works::
89
90
sage: parse_optional_tags("'ěščřžýáíéďĎ'")
91
set([])
92
"""
93
safe, literals, state = strip_string_literals(string)
94
first_line = safe.split('\n', 1)[0]
95
if '#' not in first_line:
96
return set()
97
comment = first_line[first_line.find('#')+1:]
98
comment = comment[comment.index('(')+1 : comment.rindex(')')]
99
# strip_string_literals replaces comments
100
comment = "#" + (literals[comment]).lower()
101
102
tags = []
103
for m in optional_regex.finditer(comment):
104
cmd = m.group(1)
105
if cmd == 'known bug':
106
tags.append('bug') # so that such tests will be run by sage -t ... -only-optional=bug
107
elif cmd:
108
tags.append(cmd)
109
else:
110
tags.extend(m.group(3).split() or [""])
111
return set(tags)
112
113
def parse_tolerance(source, want):
114
"""
115
Returns a version of ``want`` marked up with the tolerance tags
116
specified in ``source``.
117
118
INPUT:
119
120
- ``source`` -- a string, the source of a doctest
121
- ``want`` -- a string, the desired output of the doctest
122
123
OUTPUT:
124
125
- ``want`` if there are no tolerance tags specified; a
126
:class:`MarkedOutput` version otherwise.
127
128
EXAMPLES::
129
130
sage: from sage.doctest.parsing import parse_tolerance
131
sage: marked = parse_tolerance("sage: s.update(abs_tol = .0000001)", "")
132
sage: type(marked)
133
<type 'str'>
134
sage: marked = parse_tolerance("sage: s.update(tol = 0.1); s.rel_tol # abs tol 0.01", "")
135
sage: marked.tol
136
0
137
sage: marked.rel_tol
138
0
139
sage: marked.abs_tol
140
0.01
141
"""
142
safe, literals, state = strip_string_literals(source)
143
first_line = safe.split('\n', 1)[0]
144
if '#' not in first_line:
145
return want
146
comment = first_line[first_line.find('#')+1:]
147
comment = comment[comment.index('(')+1 : comment.rindex(')')]
148
# strip_string_literals replaces comments
149
comment = literals[comment]
150
if random_marker.search(comment):
151
want = MarkedOutput(want).update(random=True)
152
else:
153
m = tolerance_pattern.search(comment)
154
if m:
155
rel_or_abs, epsilon = m.groups()
156
if epsilon is None:
157
epsilon = 1e-15
158
else:
159
epsilon = float(epsilon.strip())
160
if rel_or_abs is None:
161
want = MarkedOutput(want).update(tol=epsilon)
162
elif rel_or_abs.startswith('rel'):
163
want = MarkedOutput(want).update(rel_tol=epsilon)
164
elif rel_or_abs.startswith('abs'):
165
want = MarkedOutput(want).update(abs_tol=epsilon)
166
else:
167
raise RuntimeError
168
return want
169
170
def pre_hash(s):
171
"""
172
Prepends a string with its length.
173
174
EXAMPLES::
175
176
sage: from sage.doctest.parsing import pre_hash
177
sage: pre_hash("abc")
178
'3:abc'
179
"""
180
return "%s:%s" % (len(s), s)
181
182
def get_source(example):
183
"""
184
Returns the source with the leading 'sage: ' stripped off.
185
186
EXAMPLES::
187
188
sage: from sage.doctest.parsing import get_source
189
sage: from sage.doctest.sources import DictAsObject
190
sage: example = DictAsObject({})
191
sage: example.sage_source = "2 + 2"
192
sage: example.source = "sage: 2 + 2"
193
sage: get_source(example)
194
'2 + 2'
195
sage: example = DictAsObject({})
196
sage: example.source = "3 + 3"
197
sage: get_source(example)
198
'3 + 3'
199
"""
200
return getattr(example, 'sage_source', example.source)
201
202
def reduce_hex(fingerprints):
203
"""
204
Returns a symmetric function of the arguments as hex strings.
205
206
The arguments should be 32 character strings consiting of hex
207
digits: 0-9 and a-f.
208
209
EXAMPLES::
210
211
sage: from sage.doctest.parsing import reduce_hex
212
sage: reduce_hex(["abc", "12399aedf"])
213
'0000000000000000000000012399a463'
214
sage: reduce_hex(["12399aedf","abc"])
215
'0000000000000000000000012399a463'
216
"""
217
from operator import xor
218
res = reduce(xor, (int(x, 16) for x in fingerprints), 0)
219
if res < 0:
220
res += 1 << 128
221
return "%032x" % res
222
223
224
class MarkedOutput(str):
225
"""
226
A subclass of string with context for whether another string
227
matches it.
228
229
EXAMPLES::
230
231
sage: from sage.doctest.parsing import MarkedOutput
232
sage: s = MarkedOutput("abc")
233
sage: s.rel_tol
234
0
235
sage: s.update(rel_tol = .05)
236
'abc'
237
sage: s.rel_tol
238
0.0500000000000000
239
"""
240
random = False
241
rel_tol = 0
242
abs_tol = 0
243
tol = 0
244
def update(self, **kwds):
245
"""
246
EXAMPLES::
247
248
sage: from sage.doctest.parsing import MarkedOutput
249
sage: s = MarkedOutput("0.0007401")
250
sage: s.update(abs_tol = .0000001)
251
'0.0007401'
252
sage: s.rel_tol
253
0
254
sage: s.abs_tol
255
1.00000000000000e-7
256
"""
257
self.__dict__.update(kwds)
258
return self
259
260
def __reduce__(self):
261
"""
262
Pickling.
263
264
EXAMPLES::
265
266
sage: from sage.doctest.parsing import MarkedOutput
267
sage: s = MarkedOutput("0.0007401")
268
sage: s.update(abs_tol = .0000001)
269
'0.0007401'
270
sage: t = loads(dumps(s)) # indirect doctest
271
sage: t == s
272
True
273
sage: t.abs_tol
274
1.00000000000000e-7
275
"""
276
return make_marked_output, (str(self), self.__dict__)
277
278
def make_marked_output(s, D):
279
"""
280
Auxilliary function for pickling.
281
282
EXAMPLES::
283
284
sage: from sage.doctest.parsing import make_marked_output
285
sage: s = make_marked_output("0.0007401",{'abs_tol':.0000001})
286
sage: s
287
'0.0007401'
288
sage: s.abs_tol
289
1.00000000000000e-7
290
"""
291
ans = MarkedOutput(s)
292
ans.__dict__.update(D)
293
return ans
294
295
class OriginalSource:
296
r"""
297
Context swapping out the pre-parsed source with the original for
298
better reporting.
299
300
EXAMPLES::
301
302
sage: from sage.doctest.sources import FileDocTestSource
303
sage: from sage.doctest.control import DocTestDefaults
304
sage: from sage.env import SAGE_SRC
305
sage: import os
306
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','forker.py')
307
sage: FDS = FileDocTestSource(filename,DocTestDefaults())
308
sage: doctests, extras = FDS.create_doctests(globals())
309
sage: ex = doctests[0].examples[0]
310
sage: ex.sage_source
311
'doctest_var = 42; doctest_var^2\n'
312
sage: ex.source
313
'doctest_var = Integer(42); doctest_var**Integer(2)\n'
314
sage: from sage.doctest.parsing import OriginalSource
315
sage: with OriginalSource(ex):
316
... ex.source
317
'doctest_var = 42; doctest_var^2\n'
318
"""
319
def __init__(self, example):
320
"""
321
Swaps out the source for the sage_source of a doctest example.
322
323
INPUT:
324
325
- ``example`` -- a :class:`doctest.Example` instance
326
327
EXAMPLES::
328
329
sage: from sage.doctest.sources import FileDocTestSource
330
sage: from sage.doctest.control import DocTestDefaults
331
sage: from sage.env import SAGE_SRC
332
sage: import os
333
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','forker.py')
334
sage: FDS = FileDocTestSource(filename,DocTestDefaults())
335
sage: doctests, extras = FDS.create_doctests(globals())
336
sage: ex = doctests[0].examples[0]
337
sage: from sage.doctest.parsing import OriginalSource
338
sage: OriginalSource(ex)
339
<sage.doctest.parsing.OriginalSource instance at ...>
340
"""
341
self.example = example
342
343
def __enter__(self):
344
r"""
345
EXAMPLES::
346
347
sage: from sage.doctest.sources import FileDocTestSource
348
sage: from sage.doctest.control import DocTestDefaults
349
sage: from sage.env import SAGE_SRC
350
sage: import os
351
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','forker.py')
352
sage: FDS = FileDocTestSource(filename,DocTestDefaults())
353
sage: doctests, extras = FDS.create_doctests(globals())
354
sage: ex = doctests[0].examples[0]
355
sage: from sage.doctest.parsing import OriginalSource
356
sage: with OriginalSource(ex): # indirect doctest
357
... ex.source
358
...
359
'doctest_var = 42; doctest_var^2\n'
360
"""
361
if hasattr(self.example, 'sage_source'):
362
self.old_source, self.example.source = self.example.source, self.example.sage_source
363
364
def __exit__(self, *args):
365
r"""
366
EXAMPLES::
367
368
sage: from sage.doctest.sources import FileDocTestSource
369
sage: from sage.doctest.control import DocTestDefaults
370
sage: from sage.env import SAGE_SRC
371
sage: import os
372
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','forker.py')
373
sage: FDS = FileDocTestSource(filename,DocTestDefaults())
374
sage: doctests, extras = FDS.create_doctests(globals())
375
sage: ex = doctests[0].examples[0]
376
sage: from sage.doctest.parsing import OriginalSource
377
sage: with OriginalSource(ex): # indirect doctest
378
... ex.source
379
...
380
'doctest_var = 42; doctest_var^2\n'
381
sage: ex.source # indirect doctest
382
'doctest_var = Integer(42); doctest_var**Integer(2)\n'
383
"""
384
if hasattr(self.example, 'sage_source'):
385
self.example.source = self.old_source
386
387
class SageDocTestParser(doctest.DocTestParser):
388
"""
389
A version of the standard doctest parser which handles Sage's
390
custom options and tolerances in floating point arithmetic.
391
"""
392
def __init__(self, long=False, optional_tags=()):
393
r"""
394
INPUT:
395
396
- ``long`` -- boolean, whether to run doctests marked as taking a long time.
397
- ``optional_tags`` -- a list or tuple of strings.
398
399
EXAMPLES::
400
401
sage: from sage.doctest.parsing import SageDocTestParser
402
sage: DTP = SageDocTestParser(True, ('sage','magma','guava'))
403
sage: ex = DTP.parse("sage: 2 + 2\n")[1]
404
sage: ex.sage_source
405
'2 + 2\n'
406
sage: ex = DTP.parse("sage: R.<x> = ZZ[]")[1]
407
sage: ex.source
408
"R = ZZ['x']; (x,) = R._first_ngens(1)\n"
409
410
TESTS::
411
412
sage: TestSuite(DTP).run()
413
"""
414
self.long = long
415
self.optionals = collections.defaultdict(int) # record skipped optional tests
416
if optional_tags is True: # run all optional tests
417
self.optional_tags = True
418
self.optional_only = False
419
else:
420
self.optional_tags = set(optional_tags)
421
if 'sage' in self.optional_tags:
422
self.optional_only = False
423
self.optional_tags.remove('sage')
424
else:
425
self.optional_only = True
426
427
def __cmp__(self, other):
428
"""
429
Comparison.
430
431
EXAMPLES::
432
433
sage: from sage.doctest.parsing import SageDocTestParser
434
sage: DTP = SageDocTestParser(True, ('sage','magma','guava'))
435
sage: DTP2 = SageDocTestParser(False, ('sage','magma','guava'))
436
sage: DTP == DTP2
437
False
438
"""
439
c = cmp(type(self), type(other))
440
if c: return c
441
return cmp(self.__dict__, other.__dict__)
442
443
def parse(self, string, *args):
444
r"""
445
A Sage specialization of :class:`doctest.DocTestParser`.
446
447
INPUTS:
448
449
- ``string`` -- the string to parse.
450
- ``name`` -- optional string giving the name indentifying string,
451
to be used in error messages.
452
453
OUTPUTS:
454
455
- A list consisting of strings and :class:`doctest.Example`
456
instances. There will be at least one string between
457
successive examples (exactly one unless or long or optional
458
tests are removed), and it will begin and end with a string.
459
460
EXAMPLES::
461
462
sage: from sage.doctest.parsing import SageDocTestParser
463
sage: DTP = SageDocTestParser(True, ('sage','magma','guava'))
464
sage: example = 'Explanatory text::\n\n sage: E = magma("EllipticCurve([1, 1, 1, -10, -10])") # optional: magma\n\nLater text'
465
sage: parsed = DTP.parse(example)
466
sage: parsed[0]
467
'Explanatory text::\n\n'
468
sage: parsed[1].sage_source
469
'E = magma("EllipticCurve([1, 1, 1, -10, -10])") # optional: magma\n'
470
sage: parsed[2]
471
'\nLater text'
472
473
If the doctest parser is not created to accept a given
474
optional argument, the corresponding examples will just be
475
removed::
476
477
sage: DTP2 = SageDocTestParser(True, ('sage',))
478
sage: parsed2 = DTP2.parse(example)
479
sage: parsed2
480
['Explanatory text::\n\n', '\nLater text']
481
482
You can mark doctests as having a particular tolerance::
483
484
sage: example2 = 'sage: gamma(1.6) # tol 2.0e-11\n0.893515349287690'
485
sage: ex = DTP.parse(example2)[1]
486
sage: ex.sage_source
487
'gamma(1.6) # tol 2.0e-11\n'
488
sage: ex.want
489
'0.893515349287690\n'
490
sage: type(ex.want)
491
<class 'sage.doctest.parsing.MarkedOutput'>
492
sage: ex.want.tol
493
2e-11
494
495
You can use continuation lines:
496
497
sage: s = "sage: for i in range(4):\n....: print i\n....:\n"
498
sage: ex = DTP2.parse(s)[1]
499
sage: ex.source
500
'for i in range(Integer(4)):\n print i\n'
501
502
Sage currently accepts backslashes as indicating that the end
503
of the current line should be joined to the next line. This
504
feature allows for breaking large integers over multiple lines
505
but is not standard for Python doctesting. It's not
506
guaranteed to persist, but works in Sage 5.5::
507
508
sage: n = 1234\
509
....: 5678
510
sage: print n
511
12345678
512
sage: type(n)
513
<type 'sage.rings.integer.Integer'>
514
515
It also works without the line continuation::
516
517
sage: m = 8765\
518
4321
519
sage: print m
520
87654321
521
"""
522
# Hack for non-standard backslash line escapes accepted by the current
523
# doctest system.
524
m = backslash_replacer.search(string)
525
while m is not None:
526
next_prompt = find_sage_prompt.search(string,m.end())
527
g = m.groups()
528
if next_prompt:
529
future = string[m.end():next_prompt.start()] + '\n' + string[next_prompt.start():]
530
else:
531
future = string[m.end():]
532
string = string[:m.start()] + g[0] + "sage:" + g[1] + future
533
m = backslash_replacer.search(string,m.start())
534
535
string = find_sage_prompt.sub(r"\1>>> sage: ", string)
536
string = find_sage_continuation.sub(r"\1...", string)
537
res = doctest.DocTestParser.parse(self, string, *args)
538
filtered = []
539
for item in res:
540
if isinstance(item, doctest.Example):
541
optional_tags = parse_optional_tags(item.source)
542
if optional_tags:
543
for tag in optional_tags:
544
self.optionals[tag] += 1
545
if ('not implemented' in optional_tags) or ('not tested' in optional_tags):
546
continue
547
if 'long time' in optional_tags:
548
if self.long:
549
optional_tags.remove('long time')
550
else:
551
continue
552
if not (self.optional_tags is True or optional_tags.issubset(self.optional_tags)):
553
continue
554
elif self.optional_only:
555
self.optionals['sage'] += 1
556
continue
557
item.want = parse_tolerance(item.source, item.want)
558
if item.source.startswith("sage: "):
559
item.sage_source = item.source[6:]
560
if item.sage_source.lstrip().startswith('#'):
561
continue
562
item.source = preparse(item.sage_source)
563
filtered.append(item)
564
return filtered
565
566
class SageOutputChecker(doctest.OutputChecker):
567
r"""
568
A modification of the doctest OutputChecker that can check
569
relative and absolute tolerance of answers.
570
571
EXAMPLES::
572
573
sage: from sage.doctest.parsing import SageOutputChecker, MarkedOutput, SageDocTestParser
574
sage: import doctest
575
sage: optflag = doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS
576
sage: DTP = SageDocTestParser(True, ('sage','magma','guava'))
577
sage: OC = SageOutputChecker()
578
sage: example2 = 'sage: gamma(1.6) # tol 2.0e-11\n0.893515349287690'
579
sage: ex = DTP.parse(example2)[1]
580
sage: ex.sage_source
581
'gamma(1.6) # tol 2.0e-11\n'
582
sage: ex.want
583
'0.893515349287690\n'
584
sage: type(ex.want)
585
<class 'sage.doctest.parsing.MarkedOutput'>
586
sage: ex.want.tol
587
2e-11
588
sage: OC.check_output(ex.want, '0.893515349287690', optflag)
589
True
590
sage: OC.check_output(ex.want, '0.8935153492877', optflag)
591
True
592
sage: OC.check_output(ex.want, '0', optflag)
593
False
594
sage: OC.check_output(ex.want, 'x + 0.8935153492877', optflag)
595
False
596
"""
597
def human_readable_escape_sequences(self, string):
598
r"""
599
Make ANSI escape sequences human readable.
600
601
EXAMPLES::
602
603
sage: print 'This is \x1b[1mbold\x1b[0m text'
604
This is <CSI-1m>bold<CSI-0m> text
605
606
TESTS::
607
608
sage: from sage.doctest.parsing import SageOutputChecker
609
sage: OC = SageOutputChecker()
610
sage: teststr = '-'.join([
611
....: 'bold\x1b[1m',
612
....: 'red\x1b[31m',
613
....: 'oscmd\x1ba'])
614
sage: OC.human_readable_escape_sequences(teststr)
615
'bold<CSI-1m>-red<CSI-31m>-oscmd<ESC-a>'
616
"""
617
def human_readable(match):
618
ansi_escape = match.group(1)
619
assert len(ansi_escape) >= 2
620
if len(ansi_escape) == 2:
621
return '<ESC-'+ansi_escape[1]+'>'
622
else:
623
return '<CSI-'+ansi_escape.lstrip('\x1b[\x9b')+'>'
624
return ansi_escape_sequence.subn(human_readable, string)[0]
625
626
def check_output(self, want, got, optionflags):
627
"""
628
Checks to see if the output matches the desired output.
629
630
If ``want`` is a :class:`MarkedOutput` instance, takes into account the desired tolerance.
631
632
INPUT:
633
634
- ``want`` -- a string or :class:`MarkedOutput`
635
- ``got`` -- a string
636
- ``optionflags`` -- an integer, passed down to :class:`doctest.OutputChecker`
637
638
OUTPUT:
639
640
- boolean, whether ``got`` matches ``want`` up to the specified tolerance.
641
642
EXAMPLES::
643
644
sage: from sage.doctest.parsing import MarkedOutput, SageOutputChecker
645
sage: import doctest
646
sage: optflag = doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS
647
sage: rndstr = MarkedOutput("I'm wrong!").update(random=True)
648
sage: tentol = MarkedOutput("10.0").update(tol=.1)
649
sage: tenabs = MarkedOutput("10.0").update(abs_tol=.1)
650
sage: tenrel = MarkedOutput("10.0").update(rel_tol=.1)
651
sage: zerotol = MarkedOutput("0.0").update(tol=.1)
652
sage: zeroabs = MarkedOutput("0.0").update(abs_tol=.1)
653
sage: zerorel = MarkedOutput("0.0").update(rel_tol=.1)
654
sage: zero = "0.0"
655
sage: nf = "9.5"
656
sage: ten = "10.05"
657
sage: eps = "-0.05"
658
sage: OC = SageOutputChecker()
659
660
::
661
662
sage: OC.check_output(rndstr,nf,optflag)
663
True
664
665
sage: OC.check_output(tentol,nf,optflag)
666
True
667
sage: OC.check_output(tentol,ten,optflag)
668
True
669
sage: OC.check_output(tentol,zero,optflag)
670
False
671
672
sage: OC.check_output(tenabs,nf,optflag)
673
False
674
sage: OC.check_output(tenabs,ten,optflag)
675
True
676
sage: OC.check_output(tenabs,zero,optflag)
677
False
678
679
sage: OC.check_output(tenrel,nf,optflag)
680
True
681
sage: OC.check_output(tenrel,ten,optflag)
682
True
683
sage: OC.check_output(tenrel,zero,optflag)
684
False
685
686
sage: OC.check_output(zerotol,zero,optflag)
687
True
688
sage: OC.check_output(zerotol,eps,optflag)
689
True
690
sage: OC.check_output(zerotol,ten,optflag)
691
False
692
693
sage: OC.check_output(zeroabs,zero,optflag)
694
True
695
sage: OC.check_output(zeroabs,eps,optflag)
696
True
697
sage: OC.check_output(zeroabs,ten,optflag)
698
False
699
700
sage: OC.check_output(zerorel,zero,optflag)
701
True
702
sage: OC.check_output(zerorel,eps,optflag)
703
False
704
sage: OC.check_output(zerorel,ten,optflag)
705
False
706
707
TESTS:
708
709
More explicit tolerance checks::
710
711
sage: _ = x # rel tol 1e10
712
sage: raise RuntimeError # rel tol 1e10
713
Traceback (most recent call last):
714
...
715
RuntimeError
716
sage: 1 # abs tol 2
717
-0.5
718
sage: print "1.000009" # abs tol 1e-5
719
1.0
720
"""
721
got = self.human_readable_escape_sequences(got)
722
if isinstance(want, MarkedOutput):
723
if want.random:
724
return True
725
elif want.tol or want.rel_tol or want.abs_tol:
726
if want.tol:
727
check_tol = lambda a, b: (a == 0 and abs(b) < want.tol) or (a*b != 0 and abs(a-b)/abs(a) < want.tol)
728
elif want.abs_tol:
729
check_tol = lambda a, b: abs(a-b) < want.abs_tol
730
else:
731
check_tol = lambda a, b: (a == b == 0) or (a*b != 0 and abs(a-b)/abs(a) < want.rel_tol)
732
want_values = [float(g[0]) for g in float_regex.findall(want)]
733
got_values = [float(g[0]) for g in float_regex.findall(got)]
734
if len(want_values) != len(got_values):
735
return False
736
if not doctest.OutputChecker.check_output(self,
737
float_regex.sub('*', want), float_regex.sub('*', got), optionflags):
738
return False
739
return all(check_tol(*ab) for ab in zip(want_values, got_values))
740
ok = doctest.OutputChecker.check_output(self, want, got, optionflags)
741
#sys.stderr.write(str(ok) + " want: " + repr(want) + " got: " + repr(got) + "\n")
742
return ok
743
744
def output_difference(self, example, got, optionflags):
745
r"""
746
Report on the differences between the desired result and what
747
was actually obtained.
748
749
If ``want`` is a :class:`MarkedOutput` instance, takes into account the desired tolerance.
750
751
INPUT:
752
753
- ``example`` -- a :class:`doctest.Example` instance
754
- ``got`` -- a string
755
- ``optionflags`` -- an integer, passed down to :class:`doctest.OutputChecker`
756
757
OUTPUT:
758
759
- a string, describing how ``got`` fails to match ``example.want``
760
761
EXAMPLES::
762
763
sage: from sage.doctest.parsing import MarkedOutput, SageOutputChecker
764
sage: import doctest
765
sage: optflag = doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS
766
sage: tentol = doctest.Example('',MarkedOutput("10.0\n").update(tol=.1))
767
sage: tenabs = doctest.Example('',MarkedOutput("10.0\n").update(abs_tol=.1))
768
sage: tenrel = doctest.Example('',MarkedOutput("10.0\n").update(rel_tol=.1))
769
sage: zerotol = doctest.Example('',MarkedOutput("0.0\n").update(tol=.1))
770
sage: zeroabs = doctest.Example('',MarkedOutput("0.0\n").update(abs_tol=.1))
771
sage: zerorel = doctest.Example('',MarkedOutput("0.0\n").update(rel_tol=.1))
772
sage: tlist = doctest.Example('',MarkedOutput("[10.0, 10.0, 10.0, 10.0, 10.0, 10.0]\n").update(abs_tol=1.0))
773
sage: zero = "0.0"
774
sage: nf = "9.5"
775
sage: ten = "10.05"
776
sage: eps = "-0.05"
777
sage: L = "[9.9, 8.7, 10.3, 11.2, 10.8, 10.0]"
778
sage: OC = SageOutputChecker()
779
780
::
781
782
sage: print OC.output_difference(tenabs,nf,optflag)
783
Expected:
784
10.0
785
Got:
786
9.5
787
Tolerance exceeded: 5e-01 > 1e-01
788
789
sage: print OC.output_difference(tentol,zero,optflag)
790
Expected:
791
10.0
792
Got:
793
0.0
794
Tolerance exceeded: infinity > 1e-01
795
796
sage: print OC.output_difference(tentol,eps,optflag)
797
Expected:
798
10.0
799
Got:
800
-0.05
801
Tolerance exceeded: 1e+00 > 1e-01
802
803
sage: print OC.output_difference(tlist,L,optflag)
804
Expected:
805
[10.0, 10.0, 10.0, 10.0, 10.0, 10.0]
806
Got:
807
[9.9, 8.7, 10.3, 11.2, 10.8, 10.0]
808
Tolerance exceeded in 2 of 6
809
10.0 vs 8.7
810
10.0 vs 11.2
811
812
813
TESTS::
814
815
sage: print OC.output_difference(tenabs,zero,optflag)
816
Expected:
817
10.0
818
Got:
819
0.0
820
Tolerance exceeded: 1e+01 > 1e-01
821
822
sage: print OC.output_difference(tenrel,zero,optflag)
823
Expected:
824
10.0
825
Got:
826
0.0
827
Tolerance exceeded: 1e+00 > 1e-01
828
829
sage: print OC.output_difference(tenrel,eps,optflag)
830
Expected:
831
10.0
832
Got:
833
-0.05
834
Tolerance exceeded: 1e+00 > 1e-01
835
836
sage: print OC.output_difference(zerotol,ten,optflag)
837
Expected:
838
0.0
839
Got:
840
10.05
841
Tolerance exceeded: 1e+01 > 1e-01
842
843
sage: print OC.output_difference(zeroabs,ten,optflag)
844
Expected:
845
0.0
846
Got:
847
10.05
848
Tolerance exceeded: 1e+01 > 1e-01
849
850
sage: print OC.output_difference(zerorel,eps,optflag)
851
Expected:
852
0.0
853
Got:
854
-0.05
855
Tolerance exceeded: infinity > 1e-01
856
857
sage: print OC.output_difference(zerorel,ten,optflag)
858
Expected:
859
0.0
860
Got:
861
10.05
862
Tolerance exceeded: infinity > 1e-01
863
864
"""
865
got = self.human_readable_escape_sequences(got)
866
want = example.want
867
diff = doctest.OutputChecker.output_difference(self, example, got, optionflags)
868
if isinstance(want, MarkedOutput) and (want.tol or want.abs_tol or want.rel_tol):
869
if diff[-1] != "\n":
870
diff += "\n"
871
want_str = [g[0] for g in float_regex.findall(want)]
872
got_str = [g[0] for g in float_regex.findall(got)]
873
want_values = [float(g) for g in want_str]
874
got_values = [float(g) for g in got_str]
875
starwant = float_regex.sub('*', want)
876
stargot = float_regex.sub('*', got)
877
if len(want_values) > 0 and len(want_values) == len(got_values) and \
878
doctest.OutputChecker.check_output(self, starwant, stargot, optionflags):
879
def failstr(c,d,actual,desired):
880
if len(want_values) == 1:
881
if actual != 'infinity':
882
actual = "%.0e"%(actual)
883
return "Tolerance exceeded: %s > %.0e\n"%(actual, desired)
884
else:
885
return " %s vs %s"%(c,d)
886
fails = []
887
for a, b, c, d in zip(want_values, got_values, want_str, got_str):
888
if want.tol:
889
if a == 0:
890
if abs(b) >= want.tol:
891
fails.append(failstr(c,d,abs(b),want.tol))
892
elif b == 0:
893
fails.append(failstr(c,d,"infinity",want.tol))
894
elif abs((a - b) / a) >= want.tol:
895
fails.append(failstr(c,d,abs((a - b) / a), want.tol))
896
elif want.abs_tol:
897
if abs(a - b) >= want.abs_tol:
898
fails.append(failstr(c,d,abs(a - b),want.abs_tol))
899
elif a == 0:
900
if b != 0:
901
fails.append(failstr(c,d,"infinity",want.rel_tol))
902
elif abs((a - b) / a) >= want.rel_tol:
903
fails.append(failstr(c,d,abs((a - b) / a),want.rel_tol))
904
if len(want_values) == 1 and len(fails) == 1: # fails should always be 1...
905
diff += fails[0]
906
else:
907
diff += "Tolerance exceeded in %s of %s\n"%(len(fails), len(want_values))
908
diff += "\n".join(fails[:3]) + "\n"
909
return diff
910
911