passagemath-repl 10.4.62__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. passagemath_repl-10.4.62.data/scripts/sage-cachegrind +25 -0
  2. passagemath_repl-10.4.62.data/scripts/sage-callgrind +16 -0
  3. passagemath_repl-10.4.62.data/scripts/sage-cleaner +230 -0
  4. passagemath_repl-10.4.62.data/scripts/sage-coverage +327 -0
  5. passagemath_repl-10.4.62.data/scripts/sage-eval +14 -0
  6. passagemath_repl-10.4.62.data/scripts/sage-fixdoctests +708 -0
  7. passagemath_repl-10.4.62.data/scripts/sage-inline-fortran +12 -0
  8. passagemath_repl-10.4.62.data/scripts/sage-ipynb2rst +50 -0
  9. passagemath_repl-10.4.62.data/scripts/sage-ipython +16 -0
  10. passagemath_repl-10.4.62.data/scripts/sage-massif +25 -0
  11. passagemath_repl-10.4.62.data/scripts/sage-notebook +267 -0
  12. passagemath_repl-10.4.62.data/scripts/sage-omega +25 -0
  13. passagemath_repl-10.4.62.data/scripts/sage-preparse +302 -0
  14. passagemath_repl-10.4.62.data/scripts/sage-run +27 -0
  15. passagemath_repl-10.4.62.data/scripts/sage-run-cython +10 -0
  16. passagemath_repl-10.4.62.data/scripts/sage-runtests +9 -0
  17. passagemath_repl-10.4.62.data/scripts/sage-startuptime.py +163 -0
  18. passagemath_repl-10.4.62.data/scripts/sage-valgrind +34 -0
  19. passagemath_repl-10.4.62.dist-info/METADATA +77 -0
  20. passagemath_repl-10.4.62.dist-info/RECORD +162 -0
  21. passagemath_repl-10.4.62.dist-info/WHEEL +5 -0
  22. passagemath_repl-10.4.62.dist-info/top_level.txt +1 -0
  23. sage/all__sagemath_repl.py +119 -0
  24. sage/doctest/__init__.py +4 -0
  25. sage/doctest/__main__.py +236 -0
  26. sage/doctest/all.py +4 -0
  27. sage/doctest/check_tolerance.py +261 -0
  28. sage/doctest/control.py +1727 -0
  29. sage/doctest/external.py +534 -0
  30. sage/doctest/fixtures.py +383 -0
  31. sage/doctest/forker.py +2665 -0
  32. sage/doctest/marked_output.py +102 -0
  33. sage/doctest/parsing.py +1708 -0
  34. sage/doctest/parsing_test.py +79 -0
  35. sage/doctest/reporting.py +733 -0
  36. sage/doctest/rif_tol.py +124 -0
  37. sage/doctest/sources.py +1657 -0
  38. sage/doctest/test.py +584 -0
  39. sage/doctest/tests/1second.rst +4 -0
  40. sage/doctest/tests/99seconds.rst +4 -0
  41. sage/doctest/tests/abort.rst +5 -0
  42. sage/doctest/tests/atexit.rst +7 -0
  43. sage/doctest/tests/fail_and_die.rst +6 -0
  44. sage/doctest/tests/initial.rst +15 -0
  45. sage/doctest/tests/interrupt.rst +7 -0
  46. sage/doctest/tests/interrupt_diehard.rst +14 -0
  47. sage/doctest/tests/keyboardinterrupt.rst +11 -0
  48. sage/doctest/tests/longtime.rst +5 -0
  49. sage/doctest/tests/nodoctest +5 -0
  50. sage/doctest/tests/random_seed.rst +4 -0
  51. sage/doctest/tests/show_skipped.rst +18 -0
  52. sage/doctest/tests/sig_on.rst +9 -0
  53. sage/doctest/tests/simple_failure.rst +8 -0
  54. sage/doctest/tests/sleep_and_raise.rst +106 -0
  55. sage/doctest/tests/tolerance.rst +31 -0
  56. sage/doctest/util.py +750 -0
  57. sage/interfaces/cleaner.py +48 -0
  58. sage/interfaces/quit.py +163 -0
  59. sage/misc/all__sagemath_repl.py +51 -0
  60. sage/misc/banner.py +235 -0
  61. sage/misc/benchmark.py +221 -0
  62. sage/misc/classgraph.py +131 -0
  63. sage/misc/copying.py +22 -0
  64. sage/misc/cython.py +694 -0
  65. sage/misc/dev_tools.py +745 -0
  66. sage/misc/edit_module.py +304 -0
  67. sage/misc/explain_pickle.py +3079 -0
  68. sage/misc/gperftools.py +361 -0
  69. sage/misc/inline_fortran.py +212 -0
  70. sage/misc/messaging.py +86 -0
  71. sage/misc/pager.py +21 -0
  72. sage/misc/profiler.py +179 -0
  73. sage/misc/python.py +70 -0
  74. sage/misc/remote_file.py +53 -0
  75. sage/misc/sage_eval.py +246 -0
  76. sage/misc/sage_input.py +3621 -0
  77. sage/misc/sagedoc.py +1742 -0
  78. sage/misc/sh.py +38 -0
  79. sage/misc/trace.py +90 -0
  80. sage/repl/__init__.py +16 -0
  81. sage/repl/all.py +15 -0
  82. sage/repl/attach.py +625 -0
  83. sage/repl/configuration.py +186 -0
  84. sage/repl/display/__init__.py +1 -0
  85. sage/repl/display/fancy_repr.py +354 -0
  86. sage/repl/display/formatter.py +318 -0
  87. sage/repl/display/jsmol_iframe.py +290 -0
  88. sage/repl/display/pretty_print.py +153 -0
  89. sage/repl/display/util.py +163 -0
  90. sage/repl/image.py +302 -0
  91. sage/repl/inputhook.py +91 -0
  92. sage/repl/interface_magic.py +298 -0
  93. sage/repl/interpreter.py +854 -0
  94. sage/repl/ipython_extension.py +593 -0
  95. sage/repl/ipython_kernel/__init__.py +1 -0
  96. sage/repl/ipython_kernel/__main__.py +4 -0
  97. sage/repl/ipython_kernel/all_jupyter.py +10 -0
  98. sage/repl/ipython_kernel/install.py +301 -0
  99. sage/repl/ipython_kernel/interact.py +278 -0
  100. sage/repl/ipython_kernel/kernel.py +217 -0
  101. sage/repl/ipython_kernel/widgets.py +466 -0
  102. sage/repl/ipython_kernel/widgets_sagenb.py +587 -0
  103. sage/repl/ipython_tests.py +163 -0
  104. sage/repl/load.py +326 -0
  105. sage/repl/preparse.py +2218 -0
  106. sage/repl/prompts.py +90 -0
  107. sage/repl/rich_output/__init__.py +4 -0
  108. sage/repl/rich_output/backend_base.py +648 -0
  109. sage/repl/rich_output/backend_doctest.py +316 -0
  110. sage/repl/rich_output/backend_emacs.py +151 -0
  111. sage/repl/rich_output/backend_ipython.py +596 -0
  112. sage/repl/rich_output/buffer.py +311 -0
  113. sage/repl/rich_output/display_manager.py +829 -0
  114. sage/repl/rich_output/example.avi +0 -0
  115. sage/repl/rich_output/example.canvas3d +1 -0
  116. sage/repl/rich_output/example.dvi +0 -0
  117. sage/repl/rich_output/example.flv +0 -0
  118. sage/repl/rich_output/example.gif +0 -0
  119. sage/repl/rich_output/example.jpg +0 -0
  120. sage/repl/rich_output/example.mkv +0 -0
  121. sage/repl/rich_output/example.mov +0 -0
  122. sage/repl/rich_output/example.mp4 +0 -0
  123. sage/repl/rich_output/example.ogv +0 -0
  124. sage/repl/rich_output/example.pdf +0 -0
  125. sage/repl/rich_output/example.png +0 -0
  126. sage/repl/rich_output/example.svg +54 -0
  127. sage/repl/rich_output/example.webm +0 -0
  128. sage/repl/rich_output/example.wmv +0 -0
  129. sage/repl/rich_output/example_jmol.spt.zip +0 -0
  130. sage/repl/rich_output/example_wavefront_scene.mtl +7 -0
  131. sage/repl/rich_output/example_wavefront_scene.obj +17 -0
  132. sage/repl/rich_output/output_basic.py +391 -0
  133. sage/repl/rich_output/output_browser.py +103 -0
  134. sage/repl/rich_output/output_catalog.py +54 -0
  135. sage/repl/rich_output/output_graphics.py +320 -0
  136. sage/repl/rich_output/output_graphics3d.py +345 -0
  137. sage/repl/rich_output/output_video.py +231 -0
  138. sage/repl/rich_output/preferences.py +432 -0
  139. sage/repl/rich_output/pretty_print.py +339 -0
  140. sage/repl/rich_output/test_backend.py +201 -0
  141. sage/repl/user_globals.py +214 -0
  142. sage/tests/__init__.py +1 -0
  143. sage/tests/all.py +3 -0
  144. sage/tests/article_heuberger_krenn_kropf_fsm-in-sage.py +630 -0
  145. sage/tests/arxiv_0812_2725.py +351 -0
  146. sage/tests/benchmark.py +1923 -0
  147. sage/tests/book_schilling_zabrocki_kschur_primer.py +795 -0
  148. sage/tests/book_stein_ent.py +651 -0
  149. sage/tests/book_stein_modform.py +558 -0
  150. sage/tests/cmdline.py +790 -0
  151. sage/tests/combinatorial_hopf_algebras.py +52 -0
  152. sage/tests/finite_poset.py +623 -0
  153. sage/tests/functools_partial_src.py +27 -0
  154. sage/tests/gosper-sum.py +218 -0
  155. sage/tests/lazy_imports.py +28 -0
  156. sage/tests/modular_group_cohomology.py +80 -0
  157. sage/tests/numpy.py +21 -0
  158. sage/tests/parigp.py +76 -0
  159. sage/tests/startup.py +27 -0
  160. sage/tests/symbolic-series.py +76 -0
  161. sage/tests/sympy.py +16 -0
  162. sage/tests/test_deprecation.py +31 -0
@@ -0,0 +1,1727 @@
1
+ # sage_setup: distribution = sagemath-repl
2
+ # sage.doctest: needs sage.all
3
+ """
4
+ Classes involved in doctesting
5
+
6
+ This module controls the various classes involved in doctesting.
7
+
8
+ AUTHORS:
9
+
10
+ - David Roe (2012-03-27) -- initial version, based on Robert Bradshaw's code.
11
+ """
12
+ # ****************************************************************************
13
+ # Copyright (C) 2012-2013 David Roe <roed.math@gmail.com>
14
+ # 2012-2013 Robert Bradshaw <robertwb@gmail.com>
15
+ # 2012 William Stein <wstein@gmail.com>
16
+ # 2013 R. Andrew Ohana
17
+ # 2013-2014 Volker Braun
18
+ # 2013-2018 Jeroen Demeyer <jdemeyer@cage.ugent.be>
19
+ # 2013-2021 John H. Palmieri
20
+ # 2017 Erik M. Bray
21
+ # 2017-2021 Frédéric Chapoton
22
+ # 2018 Sébastien Labbé
23
+ # 2019 François Bissey
24
+ # 2020-2023 Matthias Koeppe
25
+ # 2022 Michael Orlitzky
26
+ # 2022 Sebastian Oehms
27
+ #
28
+ # This program is free software: you can redistribute it and/or modify
29
+ # it under the terms of the GNU General Public License as published by
30
+ # the Free Software Foundation, either version 2 of the License, or
31
+ # (at your option) any later version.
32
+ # https://www.gnu.org/licenses/
33
+ # ****************************************************************************
34
+
35
+ import importlib
36
+ import random
37
+ import os
38
+ import sys
39
+ import time
40
+ import json
41
+ import shlex
42
+ import types
43
+ import sage.misc.flatten
44
+ import sage.misc.randstate as randstate
45
+ from sage.structure.sage_object import SageObject
46
+ from sage.env import DOT_SAGE, SAGE_LIB, SAGE_SRC, SAGE_VENV, SAGE_EXTCODE
47
+ from sage.misc.temporary_file import tmp_dir
48
+ from cysignals.signals import AlarmInterrupt, init_cysignals
49
+
50
+ from .sources import FileDocTestSource, DictAsObject, get_basename
51
+ from .forker import DocTestDispatcher
52
+ from .reporting import DocTestReporter
53
+ from .util import Timer, count_noun, dict_difference
54
+ from .external import available_software
55
+ from .parsing import parse_optional_tags, parse_file_optional_tags, unparse_optional_tags, \
56
+ nodoctest_regex, optionaltag_regex, optionalfiledirective_regex
57
+
58
+
59
+ # Optional tags which are always automatically added
60
+
61
+ auto_optional_tags = set()
62
+
63
+ class DocTestDefaults(SageObject):
64
+ """
65
+ This class is used for doctesting the Sage doctest module.
66
+
67
+ INPUT:
68
+
69
+ - ``runtest_default`` -- (boolean, default ``False``); if ``True``,
70
+ fills in attribute to be the same as the defaults defined in
71
+ ``sage-runtests``. If ``False``, change defaults in a few places
72
+ for use in doctests of the doctester, which is mostly to make
73
+ doctesting more predictable.
74
+
75
+ - ``**kwds`` -- attributes to override defaults
76
+
77
+ EXAMPLES::
78
+
79
+ sage: from sage.doctest.control import DocTestDefaults
80
+ sage: D = DocTestDefaults(); D
81
+ DocTestDefaults()
82
+ sage: D.timeout
83
+ -1
84
+
85
+ Keyword arguments become attributes::
86
+
87
+ sage: D = DocTestDefaults(timeout=100); D
88
+ DocTestDefaults(timeout=100)
89
+ sage: D.timeout
90
+ 100
91
+
92
+ The defaults for ``sage-runtests``::
93
+
94
+ sage: D = DocTestDefaults(runtest_default=True); D
95
+ DocTestDefaults(abspath=False, file_iterations=0, global_iterations=0,
96
+ optional='sage,optional', random_seed=None,
97
+ stats_path='.../timings2.json')
98
+ """
99
+ def __init__(self, runtest_default=False, **kwds):
100
+ """
101
+ Edit these parameters after creating an instance.
102
+
103
+ EXAMPLES::
104
+
105
+ sage: from sage.doctest.control import DocTestDefaults
106
+ sage: D = DocTestDefaults()
107
+ sage: 'sage' in D.optional
108
+ True
109
+ """
110
+ # NOTE that these are NOT the defaults used by the sage-runtests
111
+ # script (which is what gets invoked when running `sage -t`).
112
+ # These are only basic defaults when invoking the doctest runner
113
+ # from Python, which is not the typical use case.
114
+ self.nthreads = 1
115
+ self.serial = False
116
+ self.timeout = -1
117
+ self.die_timeout = -1
118
+ self.all = False
119
+ self.installed = False
120
+ self.logfile = None
121
+ self.long = False
122
+ self.warn_long = -1.0
123
+ self.randorder = None
124
+ self.random_seed = None if runtest_default else 0
125
+ self.global_iterations = 0 if runtest_default else 1
126
+ self.file_iterations = 0 if runtest_default else 1
127
+ self.environment = "sage.repl.ipython_kernel.all_jupyter"
128
+ self.initial = False
129
+ self.exitfirst = False
130
+ self.force_lib = False
131
+ self.if_installed = False
132
+ self.abspath = not runtest_default
133
+ self.verbose = False
134
+ self.debug = False
135
+ self.only_errors = False
136
+ self.gdb = False
137
+ self.lldb = False
138
+ self.valgrind = False
139
+ self.massif = False
140
+ self.cachegrind = False
141
+ self.omega = False
142
+ self.failed = False
143
+ self.new = False
144
+ self.show_skipped = False
145
+ self.target_walltime = -1
146
+ self.baseline_stats_path = None
147
+ self.format = "sage"
148
+
149
+ # sage-runtests contains more optional tags. Technically, adding
150
+ # auto_optional_tags here is redundant, since that is added
151
+ # automatically anyway. However, this default is still used for
152
+ # displaying user-defined optional tags and we don't want to see
153
+ # the auto_optional_tags there.
154
+ if runtest_default:
155
+ self.optional = ','.join(['sage', 'optional'])
156
+ else:
157
+ self.optional = {'sage'} | auto_optional_tags
158
+ self.hide = ''
159
+ self.probe = ''
160
+
161
+ # > 0: always run GC before every test
162
+ # < 0: disable GC
163
+ self.gc = 0
164
+
165
+ # We don't want to use the real stats file by default so that
166
+ # we don't overwrite timings for the actual running doctests.
167
+ self.stats_path = os.path.join(
168
+ DOT_SAGE, "timings2.json" if runtest_default else "timings_dt_test.json")
169
+ self.__dict__.update(kwds)
170
+
171
+ def _repr_(self):
172
+ """
173
+ Return the print representation.
174
+
175
+ EXAMPLES::
176
+
177
+ sage: from sage.doctest.control import DocTestDefaults
178
+ sage: DocTestDefaults(timeout=100, foobar='hello')
179
+ DocTestDefaults(foobar='hello', timeout=100)
180
+ """
181
+ s = "DocTestDefaults("
182
+ for k in sorted(dict_difference(self.__dict__, DocTestDefaults().__dict__).keys()):
183
+ if s[-1] != "(":
184
+ s += ", "
185
+ s += str(k) + "=" + repr(getattr(self, k))
186
+ s += ")"
187
+ return s
188
+
189
+ def __eq__(self, other):
190
+ """
191
+ Comparison by __dict__.
192
+
193
+ EXAMPLES::
194
+
195
+ sage: from sage.doctest.control import DocTestDefaults
196
+ sage: DD1 = DocTestDefaults(long=True)
197
+ sage: DD2 = DocTestDefaults(long=True)
198
+ sage: DD1 == DD2
199
+ True
200
+ """
201
+ if not isinstance(other, DocTestDefaults):
202
+ return False
203
+ return self.__dict__ == other.__dict__
204
+
205
+ def __ne__(self, other):
206
+ """
207
+ Test for non-equality.
208
+
209
+ EXAMPLES::
210
+
211
+ sage: from sage.doctest.control import DocTestDefaults
212
+ sage: DD1 = DocTestDefaults(long=True)
213
+ sage: DD2 = DocTestDefaults(long=True)
214
+ sage: DD1 != DD2
215
+ False
216
+ """
217
+ return not (self == other)
218
+
219
+
220
+ def skipdir(dirname):
221
+ """
222
+ Return ``True`` if and only if the directory ``dirname`` should not be
223
+ doctested.
224
+
225
+ EXAMPLES::
226
+
227
+ sage: from sage.doctest.control import skipdir
228
+ sage: skipdir(sage.env.SAGE_SRC)
229
+ False
230
+ sage: skipdir(os.path.join(sage.env.SAGE_SRC, "sage", "doctest", "tests"))
231
+ True
232
+ """
233
+ if os.path.exists(os.path.join(dirname, "nodoctest.py")) or os.path.exists(os.path.join(dirname, "nodoctest")):
234
+ return True
235
+ return False
236
+
237
+
238
+ def skipfile(filename, tested_optional_tags=False, *,
239
+ if_installed=False, log=None):
240
+ """
241
+ Return ``True`` if and only if the file ``filename`` should not be doctested.
242
+
243
+ INPUT:
244
+
245
+ - ``filename`` -- name of a file
246
+
247
+ - ``tested_optional_tags`` -- list or tuple or set of optional tags to test,
248
+ or ``False`` (no optional test) or ``True`` (all optional tests)
249
+
250
+ - ``if_installed`` -- boolean (default: ``False``); whether to skip Python/Cython files
251
+ that are not installed as modules
252
+
253
+ - ``log`` -- function to call with log messages, or ``None``
254
+
255
+ If ``filename`` contains a line of the form ``"# sage.doctest:
256
+ optional - xyz")``, then this will return ``False`` if "xyz" is in
257
+ ``tested_optional_tags``. Otherwise, it returns the matching tag
258
+ ("optional - xyz").
259
+
260
+ EXAMPLES::
261
+
262
+ sage: from sage.doctest.control import skipfile
263
+ sage: skipfile("skipme.c")
264
+ True
265
+ sage: filename = tmp_filename(ext='.pyx')
266
+ sage: skipfile(filename)
267
+ False
268
+ sage: with open(filename, "w") as f:
269
+ ....: _ = f.write("# nodoctest")
270
+ sage: skipfile(filename)
271
+ True
272
+ sage: with open(filename, "w") as f:
273
+ ....: _ = f.write("# sage.doctest: " # broken in two source lines to avoid the pattern
274
+ ....: "optional - xyz") # of relint (multiline_doctest_comment)
275
+ sage: skipfile(filename, False)
276
+ 'optional - xyz'
277
+ sage: bool(skipfile(filename, False))
278
+ True
279
+ sage: skipfile(filename, ['abc'])
280
+ 'optional - xyz'
281
+ sage: skipfile(filename, ['abc', 'xyz'])
282
+ False
283
+ sage: skipfile(filename, True)
284
+ False
285
+ """
286
+ if filename.endswith('__main__.py'):
287
+ if log:
288
+ log(f"Skipping '{filename}' because it is a __main__.py file")
289
+ return True
290
+ if filename.endswith('.rst.txt'):
291
+ ext = '.rst.txt'
292
+ else:
293
+ _ , ext = os.path.splitext(filename)
294
+ # .rst.txt appear in the installed documentation in subdirectories named "_sources"
295
+ if ext not in ('.py', '.pyx', '.pxd', '.pxi', '.sage', '.spyx', '.rst', '.tex', '.rst.txt'):
296
+ if log:
297
+ log(f"Skipping '{filename}' because it does not have one of the recognized file name extensions")
298
+ return True
299
+ if if_installed and ext not in ('.py', '.pyx'):
300
+ if log:
301
+ log(f"Skipping '{filename}' because it is not the source file of a Python module")
302
+ return True
303
+ if "jupyter_execute" in filename:
304
+ if log:
305
+ log(f"Skipping '{filename}' because it is created by the jupyter-sphinx extension for internal use and should not be tested")
306
+ return True
307
+ if if_installed:
308
+ module_name = get_basename(filename)
309
+ try:
310
+ if not importlib.util.find_spec(module_name): # tries to import the containing package
311
+ if log:
312
+ log(f"Skipping '{filename}' because module {module_name} is not present in the venv")
313
+ return True
314
+ except ModuleNotFoundError as e:
315
+ if log:
316
+ log(f"Skipping '{filename}' because module {e.name} cannot be imported")
317
+ return True
318
+
319
+ with open(filename) as F:
320
+ file_optional_tags = parse_file_optional_tags(enumerate(F))
321
+
322
+ if 'not tested' in file_optional_tags:
323
+ if log:
324
+ log(f"Skipping '{filename}' because it is marked 'nodoctest'")
325
+ return True
326
+
327
+ if tested_optional_tags is False:
328
+ if file_optional_tags:
329
+ file_tag_string = unparse_optional_tags(file_optional_tags, prefix='')
330
+ if log:
331
+ log(f"Skipping '{filename}' because it is marked '# {file_tag_string}'")
332
+ return file_tag_string
333
+
334
+ elif tested_optional_tags is not True:
335
+ extra = {tag for tag in file_optional_tags
336
+ if tag not in tested_optional_tags}
337
+ if extra:
338
+ file_tag_string = unparse_optional_tags(file_optional_tags, prefix='')
339
+ if log:
340
+ log(f"Skipping '{filename}' because it is marked '{file_tag_string}'")
341
+ return file_tag_string
342
+
343
+ return False
344
+
345
+
346
+ class Logger:
347
+ r"""
348
+ File-like object which implements writing to multiple files at
349
+ once.
350
+
351
+ EXAMPLES::
352
+
353
+ sage: from sage.doctest.control import Logger
354
+ sage: with open(tmp_filename(), "w+") as t:
355
+ ....: L = Logger(sys.stdout, t)
356
+ ....: _ = L.write("hello world\n")
357
+ ....: _ = t.seek(0)
358
+ ....: t.read()
359
+ hello world
360
+ 'hello world\n'
361
+ """
362
+ def __init__(self, *files):
363
+ r"""
364
+ Initialize the logger for writing to all files in ``files``.
365
+
366
+ TESTS::
367
+
368
+ sage: from sage.doctest.control import Logger
369
+ sage: Logger().write("hello world\n") # no-op
370
+ """
371
+ self.files = list(files)
372
+
373
+ def write(self, x):
374
+ r"""
375
+ Write ``x`` to all files.
376
+
377
+ TESTS::
378
+
379
+ sage: from sage.doctest.control import Logger
380
+ sage: Logger(sys.stdout).write("hello world\n")
381
+ hello world
382
+ """
383
+ for f in self.files:
384
+ f.write(x)
385
+
386
+ def flush(self):
387
+ """
388
+ Flush all files.
389
+
390
+ TESTS::
391
+
392
+ sage: from sage.doctest.control import Logger
393
+ sage: Logger(sys.stdout).flush()
394
+ """
395
+ for f in self.files:
396
+ f.flush()
397
+
398
+
399
+ class DocTestController(SageObject):
400
+ """
401
+ This class controls doctesting of files.
402
+
403
+ After creating it with appropriate options, call the :meth:`run` method to run the doctests.
404
+ """
405
+ def __init__(self, options, args):
406
+ """
407
+ Initialization.
408
+
409
+ INPUT:
410
+
411
+ - ``options`` -- either options generated from the command line by sage-runtests
412
+ or a DocTestDefaults object (possibly with some entries modified)
413
+ - ``args`` -- list of filenames to doctest
414
+
415
+ EXAMPLES::
416
+
417
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
418
+ sage: DC = DocTestController(DocTestDefaults(), [])
419
+ sage: DC
420
+ DocTest Controller
421
+ """
422
+ # First we modify options to take environment variables into
423
+ # account and check compatibility of the user's specified
424
+ # options.
425
+ if options.timeout < 0:
426
+ if options.gdb or options.lldb or options.debug:
427
+ # Interactive debuggers: "infinite" timeout
428
+ options.timeout = 0
429
+ elif options.valgrind or options.massif or options.cachegrind or options.omega:
430
+ # Non-interactive debuggers: 48 hours
431
+ options.timeout = int(os.getenv('SAGE_TIMEOUT_VALGRIND', 48 * 60 * 60))
432
+ elif options.long:
433
+ options.timeout = int(os.getenv('SAGE_TIMEOUT_LONG', 30 * 60))
434
+ else:
435
+ options.timeout = int(os.getenv('SAGE_TIMEOUT', 10 * 60))
436
+ # For non-default GC options, double the timeout
437
+ if options.gc:
438
+ options.timeout *= 2
439
+ if options.nthreads == 0:
440
+ options.nthreads = int(os.getenv('SAGE_NUM_THREADS_PARALLEL', 1))
441
+ if options.failed and not (args or options.new):
442
+ # If the user doesn't specify any files then we rerun all failed files.
443
+ options.all = True
444
+ if options.global_iterations == 0:
445
+ options.global_iterations = int(os.environ.get('SAGE_TEST_GLOBAL_ITER', 1))
446
+ if options.file_iterations == 0:
447
+ options.file_iterations = int(os.environ.get('SAGE_TEST_ITER', 1))
448
+ if options.debug:
449
+ if options.nthreads > 1:
450
+ print("Debugging requires single-threaded operation, setting number of threads to 1.")
451
+ if options.logfile:
452
+ print("Debugging is not compatible with logging, disabling logfile.")
453
+ options.serial = True
454
+ options.logfile = None
455
+ if options.serial:
456
+ options.nthreads = 1
457
+ if options.verbose:
458
+ options.show_skipped = True
459
+
460
+ options.hidden_features = set()
461
+ if isinstance(options.hide, str):
462
+ if not len(options.hide):
463
+ options.hide = set()
464
+ else:
465
+ s = options.hide.lower()
466
+ options.hide = set(s.split(','))
467
+ for h in options.hide:
468
+ if not optionaltag_regex.search(h):
469
+ raise ValueError('invalid optional tag {!r}'.format(h))
470
+ if 'all' in options.hide:
471
+ options.hide.discard('all')
472
+ from sage.features.all import all_features
473
+ feature_names = {f.name for f in all_features() if not f.is_standard()}
474
+ options.hide = options.hide.union(feature_names)
475
+ if 'optional' in options.hide:
476
+ options.hide.discard('optional')
477
+ from sage.features.all import all_features
478
+ feature_names = {f.name for f in all_features() if f.is_optional()}
479
+ options.hide = options.hide.union(feature_names)
480
+
481
+ options.disabled_optional = set()
482
+ if isinstance(options.optional, str):
483
+ s = options.optional.lower()
484
+ options.optional = set(s.split(','))
485
+ if "all" in options.optional:
486
+ # Special case to run all optional tests
487
+ options.optional = True
488
+ else:
489
+ # We replace the 'optional' tag by all optional
490
+ # packages for which the installed version matches the
491
+ # latest available version (this implies in particular
492
+ # that the package is actually installed).
493
+ if 'optional' in options.optional:
494
+ options.optional.discard('optional')
495
+ from sage.misc.package import list_packages
496
+ for pkg in list_packages('optional', local=True).values():
497
+ if pkg.name in options.hide:
498
+ continue
499
+ # Skip features for which we have a more specific runtime feature test.
500
+ if pkg.name in ['bliss', 'coxeter3', 'ecm', 'fricas', 'frobby', 'gfan', 'giac', 'jmol', 'latte_int', 'macaulay2', 'mcqd', 'meataxe', 'msolve', 'sirocco', 'tdlib']:
501
+ continue
502
+ if pkg.is_installed() and pkg.installed_version == pkg.remote_version:
503
+ options.optional.add(pkg.name)
504
+
505
+ from sage.features import package_systems
506
+ options.optional.update(system.name
507
+ for system in package_systems())
508
+ # Check that all tags are valid
509
+ for o in options.optional:
510
+ if o.startswith('!'):
511
+ if not optionaltag_regex.search(o[1:]):
512
+ raise ValueError('invalid optional tag {!r}'.format(o))
513
+ options.disabled_optional.add(o[1:])
514
+ elif not optionaltag_regex.search(o):
515
+ raise ValueError('invalid optional tag {!r}'.format(o))
516
+
517
+ options.optional |= auto_optional_tags
518
+ options.optional -= options.disabled_optional
519
+
520
+ if isinstance(options.probe, str):
521
+ if options.probe == 'none':
522
+ options.probe = ''
523
+ s = options.probe.lower()
524
+ if not s:
525
+ options.probe = set()
526
+ else:
527
+ options.probe = set(s.split(','))
528
+ if "all" in options.probe:
529
+ # Special case to probe all features that are not present
530
+ options.probe = True
531
+ else:
532
+ # Check that all tags are valid
533
+ for o in options.probe:
534
+ if not optionaltag_regex.search(o):
535
+ raise ValueError('invalid optional tag {!r}'.format(o))
536
+
537
+ self.options = options
538
+
539
+ self.files = args
540
+ if options.logfile:
541
+ if not isinstance(options.logfile, str):
542
+ # file from sage-runtests
543
+ self.logfile = options.logfile
544
+ else:
545
+ # string from DocTestDefaults
546
+ try:
547
+ self.logfile = open(options.logfile, 'a')
548
+ except OSError:
549
+ print("Unable to open logfile {!r}\nProceeding without logging.".format(options.logfile))
550
+ self.logfile = None
551
+ else:
552
+ self.logfile = None
553
+
554
+ # Flush any diagnostic messages we just printed
555
+ sys.stdout.flush()
556
+ sys.stderr.flush()
557
+
558
+ # In serial mode, we run just one process. Then the doctests
559
+ # will interfere with the output logging (both use stdout).
560
+ # To solve this, we create real_stdout which will always
561
+ # write to the actual standard output, regardless of
562
+ # redirections.
563
+ if options.serial:
564
+ self._real_stdout = os.fdopen(os.dup(sys.stdout.fileno()), "w")
565
+ self._close_stdout = True
566
+ else:
567
+ # Parallel mode: no special tricks needed
568
+ self._real_stdout = sys.stdout
569
+ self._close_stdout = False
570
+
571
+ if self.logfile is None:
572
+ self.logger = self._real_stdout
573
+ else:
574
+ self.logger = Logger(self._real_stdout, self.logfile)
575
+
576
+ self.stats = {}
577
+ self.load_stats(options.stats_path)
578
+ self.baseline_stats = {}
579
+ if options.baseline_stats_path:
580
+ self.load_baseline_stats(options.baseline_stats_path)
581
+ self._init_warn_long()
582
+
583
+ if self.options.random_seed is None:
584
+ randstate.set_random_seed()
585
+ self.options.random_seed = randstate.initial_seed()
586
+
587
+ def __del__(self):
588
+ if getattr(self, 'logfile', None) is not None:
589
+ self.logfile.close()
590
+
591
+ if getattr(self, '_close_stdout', False):
592
+ self._real_stdout.close()
593
+
594
+ def _init_warn_long(self):
595
+ """
596
+ Pick a suitable default for the ``--warn-long`` option if not
597
+ specified.
598
+
599
+ It is desirable to have all tests (even ``# long`` ones)
600
+ finish in less than about 5 seconds. Longer tests typically
601
+ don't add coverage, they just make testing slow.
602
+
603
+ The default used here is 5 seconds, unless `--long` was used,
604
+ in which case it is 30 seconds.
605
+
606
+ TESTS:
607
+
608
+ Ensure that the user's command-line options are not changed::
609
+
610
+ sage: from sage.doctest.control import (DocTestDefaults,
611
+ ....: DocTestController)
612
+ sage: DC = DocTestController(DocTestDefaults(), [])
613
+ sage: DC.options.warn_long = 5.0
614
+ sage: DC._init_warn_long()
615
+ sage: DC.options.warn_long
616
+ 5.00000000000000
617
+ """
618
+ # default is -1.0
619
+ if self.options.warn_long >= 0: # Specified on the command line
620
+ return
621
+
622
+ # The developer's guide says that even a "long time" test
623
+ # should ideally complete in under five seconds, so we're
624
+ # being rather generous here.
625
+ self.options.warn_long = 5.0
626
+ if self.options.long:
627
+ self.options.warn_long = 30.0
628
+
629
+ def second_on_modern_computer(self):
630
+ """
631
+ Return the wall time equivalent of a second on a modern computer.
632
+
633
+ OUTPUT:
634
+
635
+ Float. The wall time on your computer that would be equivalent
636
+ to one second on a modern computer. Unless you have kick-ass
637
+ hardware this should always be >= 1.0. This raises a
638
+ :exc:`RuntimeError` if there are no stored timings to use as
639
+ benchmark.
640
+
641
+ EXAMPLES::
642
+
643
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
644
+ sage: DC = DocTestController(DocTestDefaults(), [])
645
+ sage: DC.second_on_modern_computer() # not tested
646
+ """
647
+ from sage.misc.superseded import deprecation
648
+ deprecation(32981, "this method is no longer used by the sage library and will eventually be removed")
649
+
650
+ if len(self.stats) == 0:
651
+ raise RuntimeError('no stored timings available')
652
+ success = []
653
+ failed = []
654
+ for mod in self.stats.values():
655
+ if mod.get('failed', False):
656
+ failed.append(mod['walltime'])
657
+ else:
658
+ success.append(mod['walltime'])
659
+ if len(success) < 2500:
660
+ raise RuntimeError('too few successful tests, not using stored timings')
661
+ if len(failed) > 20:
662
+ raise RuntimeError('too many failed tests, not using stored timings')
663
+ expected = 12800.0 # Core i7 Quad-Core 2014
664
+ return sum(success) / expected
665
+
666
+ def _repr_(self):
667
+ """
668
+ String representation.
669
+
670
+ EXAMPLES::
671
+
672
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
673
+ sage: DC = DocTestController(DocTestDefaults(), [])
674
+ sage: repr(DC) # indirect doctest
675
+ 'DocTest Controller'
676
+ """
677
+ return "DocTest Controller"
678
+
679
+ def load_environment(self):
680
+ """
681
+ Return the module that provides the global environment.
682
+
683
+ EXAMPLES::
684
+
685
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
686
+ sage: DC = DocTestController(DocTestDefaults(), [])
687
+ sage: 'BipartiteGraph' in DC.load_environment().__dict__ # needs sage.graphs
688
+ True
689
+ sage: DC = DocTestController(DocTestDefaults(environment='sage.doctest.all'), [])
690
+ sage: 'BipartiteGraph' in DC.load_environment().__dict__ # needs sage.graphs
691
+ False
692
+ sage: 'run_doctests' in DC.load_environment().__dict__
693
+ True
694
+ """
695
+ from importlib import import_module
696
+ return import_module(self.options.environment)
697
+
698
+ def load_baseline_stats(self, filename):
699
+ """
700
+ Load baseline stats.
701
+
702
+ This must be a JSON file in the same format that :meth:`load_stats`
703
+ expects.
704
+
705
+ EXAMPLES::
706
+
707
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
708
+ sage: DC = DocTestController(DocTestDefaults(), [])
709
+ sage: import json
710
+ sage: filename = tmp_filename()
711
+ sage: with open(filename, 'w') as stats_file:
712
+ ....: json.dump({'sage.doctest.control':{'failed':True}}, stats_file)
713
+ sage: DC.load_baseline_stats(filename)
714
+ sage: DC.baseline_stats['sage.doctest.control']
715
+ {'failed': True}
716
+
717
+ If the file doesn't exist, nothing happens. If there is an
718
+ error, print a message. In any case, leave the stats alone::
719
+
720
+ sage: d = tmp_dir()
721
+ sage: DC.load_baseline_stats(os.path.join(d)) # Cannot read a directory
722
+ Error loading baseline stats from ...
723
+ sage: DC.load_baseline_stats(os.path.join(d, "no_such_file"))
724
+ sage: DC.baseline_stats['sage.doctest.control']
725
+ {'failed': True}
726
+ """
727
+ # Simply ignore non-existing files
728
+ if not os.path.exists(filename):
729
+ return
730
+
731
+ try:
732
+ with open(filename) as stats_file:
733
+ self.baseline_stats.update(json.load(stats_file))
734
+ except Exception as e:
735
+ self.log("Error loading baseline stats from %s: %s" % (filename, e))
736
+
737
+ def load_stats(self, filename):
738
+ """
739
+ Load stats from the most recent run(s).
740
+
741
+ Stats are stored as a JSON file, and include information on
742
+ which files failed tests and the walltime used for execution
743
+ of the doctests.
744
+
745
+ EXAMPLES::
746
+
747
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
748
+ sage: DC = DocTestController(DocTestDefaults(), [])
749
+ sage: import json
750
+ sage: filename = tmp_filename()
751
+ sage: with open(filename, 'w') as stats_file:
752
+ ....: json.dump({'sage.doctest.control': {'walltime': 1.0r}}, stats_file)
753
+ sage: DC.load_stats(filename)
754
+ sage: DC.stats['sage.doctest.control']
755
+ {'walltime': 1.0}
756
+
757
+ If the file doesn't exist, nothing happens. If there is an
758
+ error, print a message. In any case, leave the stats alone::
759
+
760
+ sage: d = tmp_dir()
761
+ sage: DC.load_stats(os.path.join(d)) # Cannot read a directory
762
+ Error loading stats from ...
763
+ sage: DC.load_stats(os.path.join(d, "no_such_file"))
764
+ sage: DC.stats['sage.doctest.control']
765
+ {'walltime': 1.0}
766
+ """
767
+ # Simply ignore non-existing files
768
+ if not os.path.exists(filename):
769
+ return
770
+
771
+ try:
772
+ with open(filename) as stats_file:
773
+ self.stats.update(json.load(stats_file))
774
+ except Exception:
775
+ self.log("Error loading stats from %s" % filename)
776
+
777
+ def save_stats(self, filename):
778
+ """
779
+ Save stats from the most recent run as a JSON file.
780
+
781
+ WARNING: This function overwrites the file.
782
+
783
+ EXAMPLES::
784
+
785
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
786
+ sage: DC = DocTestController(DocTestDefaults(), [])
787
+ sage: DC.stats['sage.doctest.control'] = {'walltime': 1.0r}
788
+ sage: filename = tmp_filename()
789
+ sage: DC.save_stats(filename)
790
+ sage: import json
791
+ sage: with open(filename) as f:
792
+ ....: D = json.load(f)
793
+ sage: D['sage.doctest.control']
794
+ {'walltime': 1.0}
795
+ """
796
+ from sage.misc.temporary_file import atomic_write
797
+ with atomic_write(filename) as stats_file:
798
+ json.dump(self.stats, stats_file, sort_keys=True, indent=4)
799
+
800
+ def log(self, s, end='\n'):
801
+ """
802
+ Log the string ``s + end`` (where ``end`` is a newline by default)
803
+ to the logfile and print it to the standard output.
804
+
805
+ EXAMPLES::
806
+
807
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
808
+ sage: DD = DocTestDefaults(logfile=tmp_filename())
809
+ sage: DC = DocTestController(DD, [])
810
+ sage: DC.log("hello world")
811
+ hello world
812
+ sage: DC.logfile.close()
813
+ sage: with open(DD.logfile) as f:
814
+ ....: print(f.read())
815
+ hello world
816
+
817
+ In serial mode, check that logging works even if ``stdout`` is
818
+ redirected::
819
+
820
+ sage: DD = DocTestDefaults(logfile=tmp_filename(), serial=True)
821
+ sage: DC = DocTestController(DD, [])
822
+ sage: from sage.doctest.forker import SageSpoofInOut
823
+ sage: with open(os.devnull, 'w') as devnull:
824
+ ....: S = SageSpoofInOut(devnull)
825
+ ....: S.start_spoofing()
826
+ ....: DC.log("hello world")
827
+ ....: S.stop_spoofing()
828
+ hello world
829
+ sage: DC.logfile.close()
830
+ sage: with open(DD.logfile) as f:
831
+ ....: print(f.read())
832
+ hello world
833
+
834
+ Check that no duplicate logs appear, even when forking (:issue:`15244`)::
835
+
836
+ sage: DD = DocTestDefaults(logfile=tmp_filename())
837
+ sage: DC = DocTestController(DD, [])
838
+ sage: DC.log("hello world")
839
+ hello world
840
+ sage: if os.fork() == 0:
841
+ ....: DC.logfile.close()
842
+ ....: os._exit(0)
843
+ sage: DC.logfile.close()
844
+ sage: with open(DD.logfile) as f:
845
+ ....: print(f.read())
846
+ hello world
847
+ """
848
+ self.logger.write(s + end)
849
+ self.logger.flush()
850
+
851
+ def create_run_id(self):
852
+ """
853
+ Create the run id.
854
+
855
+ EXAMPLES::
856
+
857
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
858
+ sage: DC = DocTestController(DocTestDefaults(), [])
859
+ sage: DC.create_run_id()
860
+ Running doctests with ID ...
861
+ """
862
+ self.run_id = time.strftime('%Y-%m-%d-%H-%M-%S-') + "%08x" % random.getrandbits(32)
863
+ self.log("Running doctests with ID %s." % self.run_id)
864
+
865
+ def add_files(self):
866
+ r"""
867
+ Check for the flags '--all' and '--new'.
868
+
869
+ For each one present, this function adds the appropriate directories and files to the todo list.
870
+
871
+ EXAMPLES::
872
+
873
+ sage: from sage.doctest.control import (DocTestDefaults,
874
+ ....: DocTestController)
875
+ sage: from sage.env import SAGE_SRC
876
+ sage: import tempfile
877
+ sage: with tempfile.NamedTemporaryFile() as f:
878
+ ....: DD = DocTestDefaults(all=True, logfile=f.name)
879
+ ....: DC = DocTestController(DD, [])
880
+ ....: DC.add_files()
881
+ Doctesting ...
882
+ sage: os.path.join(SAGE_SRC, 'sage') in DC.files
883
+ True
884
+
885
+ ::
886
+
887
+ sage: DD = DocTestDefaults(new = True)
888
+ sage: DC = DocTestController(DD, [])
889
+ sage: DC.add_files()
890
+ Doctesting ...
891
+ """
892
+ opj = os.path.join
893
+ from sage.env import SAGE_SRC, SAGE_DOC_SRC, SAGE_ROOT, SAGE_ROOT_GIT, SAGE_DOC
894
+ # SAGE_ROOT_GIT can be None on distributions which typically
895
+ # only have the SAGE_LOCAL install tree but not SAGE_ROOT
896
+ if SAGE_ROOT_GIT is not None:
897
+ have_git = os.path.exists(SAGE_ROOT_GIT)
898
+ else:
899
+ have_git = False
900
+
901
+ def all_installed_modules():
902
+ self.log("Doctesting all installed modules of the Sage library.")
903
+ import sage
904
+ self.files.extend(sage.__path__)
905
+ try:
906
+ import sage_setup
907
+ self.files.extend(sage_setup.__path__)
908
+ except ImportError:
909
+ pass
910
+ try:
911
+ import sage_docbuild
912
+ self.files.extend(sage_docbuild.__path__)
913
+ except ImportError:
914
+ pass
915
+
916
+ def all_installed_doc():
917
+ if SAGE_DOC and os.path.isdir(SAGE_DOC):
918
+ self.log("Doctesting all installed documentation sources.")
919
+ self.files.append(SAGE_DOC)
920
+
921
+ def all_files():
922
+ if not SAGE_SRC:
923
+ return all_installed_modules()
924
+ self.log("Doctesting entire Sage library.")
925
+ self.files.append(opj(SAGE_SRC, 'sage'))
926
+ # Only test sage_setup and sage_docbuild if the relevant
927
+ # imports work. They may not work if not in a build
928
+ # environment or if the documentation build has been
929
+ # disabled.
930
+ try:
931
+ import sage_setup
932
+ self.files.append(opj(SAGE_SRC, 'sage_setup'))
933
+ except ImportError:
934
+ pass
935
+ try:
936
+ import sage_docbuild
937
+ self.files.append(opj(SAGE_SRC, 'sage_docbuild'))
938
+ except ImportError:
939
+ pass
940
+
941
+ def all_doc_sources():
942
+ if SAGE_DOC_SRC and os.path.isdir(SAGE_DOC_SRC):
943
+ self.log("Doctesting all documentation sources.")
944
+ self.files.append(SAGE_DOC_SRC)
945
+ else:
946
+ all_installed_doc()
947
+
948
+ if self.options.installed:
949
+ all_installed_modules()
950
+ all_installed_doc()
951
+
952
+ elif self.options.all or (self.options.new and not have_git):
953
+ all_files()
954
+ all_doc_sources()
955
+
956
+ elif self.options.new and have_git:
957
+ # Get all files changed in the working repo.
958
+ self.log("Doctesting files changed since last git commit")
959
+ import subprocess
960
+ change = subprocess.check_output(["git",
961
+ "--git-dir=" + SAGE_ROOT_GIT,
962
+ "--work-tree=" + SAGE_ROOT,
963
+ "status",
964
+ "--porcelain"])
965
+ change = change.decode('utf-8')
966
+ for line in change.split("\n"):
967
+ if not line:
968
+ continue
969
+ data = line.strip().split(' ')
970
+ status, filename = data[0], data[-1]
971
+ if (set(status).issubset("MARCU")
972
+ and filename.startswith("src/sage")
973
+ and (filename.endswith(".py") or
974
+ filename.endswith(".pyx") or
975
+ filename.endswith(".rst"))
976
+ and not skipfile(opj(SAGE_ROOT, filename),
977
+ bool(self.options.optional),
978
+ if_installed=self.options.if_installed)):
979
+ self.files.append(os.path.relpath(opj(SAGE_ROOT, filename)))
980
+
981
+ def expand_files_into_sources(self):
982
+ r"""
983
+ Expand ``self.files``, which may include directories, into a
984
+ list of :class:`sage.doctest.FileDocTestSource`
985
+
986
+ This function also handles the optional command line option.
987
+
988
+ EXAMPLES::
989
+
990
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
991
+ sage: from sage.env import SAGE_SRC
992
+ sage: import os
993
+ sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
994
+ sage: DD = DocTestDefaults(optional='all')
995
+ sage: DC = DocTestController(DD, [dirname])
996
+ sage: DC.expand_files_into_sources()
997
+ sage: len(DC.sources)
998
+ 15
999
+ sage: DC.sources[0].options.optional
1000
+ True
1001
+
1002
+ ::
1003
+
1004
+ sage: DD = DocTestDefaults(optional='magma,guava')
1005
+ sage: DC = DocTestController(DD, [dirname])
1006
+ sage: DC.expand_files_into_sources()
1007
+ sage: all(t in DC.sources[0].options.optional for t in ['magma','guava'])
1008
+ True
1009
+
1010
+ We check that files are skipped appropriately::
1011
+
1012
+ sage: dirname = tmp_dir()
1013
+ sage: filename = os.path.join(dirname, 'not_tested.py')
1014
+ sage: with open(filename, 'w') as f:
1015
+ ....: _ = f.write("#"*80 + "\n\n\n\n## nodoctest\n sage: 1+1\n 4")
1016
+ sage: DC = DocTestController(DD, [dirname])
1017
+ sage: DC.expand_files_into_sources()
1018
+ sage: DC.sources
1019
+ []
1020
+
1021
+ The directory ``sage/doctest/tests`` contains ``nodoctest.py``
1022
+ but the files should still be tested when that directory is
1023
+ explicitly given (as opposed to being recursed into)::
1024
+
1025
+ sage: DC = DocTestController(DD, [os.path.join(SAGE_SRC, 'sage', 'doctest', 'tests')])
1026
+ sage: DC.expand_files_into_sources()
1027
+ sage: len(DC.sources) >= 10
1028
+ True
1029
+ """
1030
+ def expand():
1031
+ for path in self.files:
1032
+ if os.path.isdir(path):
1033
+ for root, dirs, files in os.walk(path):
1034
+ for dir in list(dirs):
1035
+ if dir[0] == "." or skipdir(os.path.join(root, dir)):
1036
+ dirs.remove(dir)
1037
+ for file in files:
1038
+ if not skipfile(os.path.join(root, file),
1039
+ bool(self.options.optional),
1040
+ if_installed=self.options.if_installed):
1041
+ yield os.path.join(root, file)
1042
+ else:
1043
+ if not skipfile(path, bool(self.options.optional),
1044
+ if_installed=self.options.if_installed, log=self.log): # log when directly specified filenames are skipped
1045
+ yield path
1046
+ self.sources = [FileDocTestSource(path, self.options) for path in expand()]
1047
+
1048
+ def filter_sources(self):
1049
+ """
1050
+
1051
+ EXAMPLES::
1052
+
1053
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1054
+ sage: from sage.env import SAGE_SRC
1055
+ sage: import os
1056
+ sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
1057
+ sage: DD = DocTestDefaults(failed=True)
1058
+ sage: DC = DocTestController(DD, [dirname])
1059
+ sage: DC.expand_files_into_sources()
1060
+ sage: for i, source in enumerate(DC.sources):
1061
+ ....: DC.stats[source.basename] = {'walltime': 0.1r * (i+1)}
1062
+ sage: DC.stats['sage.doctest.control'] = {'failed': True, 'walltime': 1.0r}
1063
+ sage: DC.filter_sources()
1064
+ Only doctesting files that failed last test.
1065
+ sage: len(DC.sources)
1066
+ 1
1067
+ """
1068
+ # Filter the sources to only include those with failing doctests if the --failed option is passed
1069
+ if self.options.failed:
1070
+ self.log("Only doctesting files that failed last test.")
1071
+
1072
+ def is_failure(source):
1073
+ basename = source.basename
1074
+ return basename not in self.stats or self.stats[basename].get('failed')
1075
+ self.sources = [x for x in self.sources if is_failure(x)]
1076
+
1077
+ def sort_sources(self):
1078
+ r"""
1079
+ This function sorts the sources so that slower doctests are run first.
1080
+
1081
+ EXAMPLES::
1082
+
1083
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1084
+ sage: from sage.env import SAGE_SRC
1085
+ sage: import os
1086
+ sage: dirname = os.path.join(SAGE_SRC, 'sage', 'doctest')
1087
+ sage: DD = DocTestDefaults(nthreads=2)
1088
+ sage: DC = DocTestController(DD, [dirname])
1089
+ sage: DC.expand_files_into_sources()
1090
+ sage: DC.sources.sort(key=lambda s:s.basename)
1091
+ sage: for i, source in enumerate(DC.sources):
1092
+ ....: DC.stats[source.basename] = {'walltime': 0.1r * (i+1)}
1093
+ sage: DC.sort_sources()
1094
+ Sorting sources by runtime so that slower doctests are run first....
1095
+ sage: print("\n".join(source.basename for source in DC.sources))
1096
+ sage.doctest.util
1097
+ sage.doctest.test
1098
+ sage.doctest.sources
1099
+ sage.doctest.rif_tol
1100
+ sage.doctest.reporting
1101
+ sage.doctest.parsing_test
1102
+ sage.doctest.parsing
1103
+ sage.doctest.marked_output
1104
+ sage.doctest.forker
1105
+ sage.doctest.fixtures
1106
+ sage.doctest.external
1107
+ sage.doctest.control
1108
+ sage.doctest.check_tolerance
1109
+ sage.doctest.all
1110
+ sage.doctest
1111
+ """
1112
+ if self.options.nthreads > 1 and len(self.sources) > self.options.nthreads:
1113
+ self.log("Sorting sources by runtime so that slower doctests are run first....")
1114
+ default = {'walltime': 0}
1115
+
1116
+ def sort_key(source):
1117
+ basename = source.basename
1118
+ return -self.stats.get(basename, default).get('walltime', 0), basename
1119
+ self.sources = sorted(self.sources, key=sort_key)
1120
+
1121
+ def source_baseline(self, source):
1122
+ r"""
1123
+ Return the ``baseline_stats`` value of ``source``.
1124
+
1125
+ INPUT:
1126
+
1127
+ - ``source`` -- a :class:`DocTestSource` instance
1128
+
1129
+ OUTPUT: a dictionary
1130
+
1131
+ EXAMPLES::
1132
+
1133
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1134
+ sage: filename = sage.doctest.util.__file__
1135
+ sage: DD = DocTestDefaults()
1136
+ sage: DC = DocTestController(DD, [filename])
1137
+ sage: DC.expand_files_into_sources()
1138
+ sage: DC.source_baseline(DC.sources[0])
1139
+ {}
1140
+ """
1141
+ if self.baseline_stats:
1142
+ basename = source.basename
1143
+ return self.baseline_stats.get(basename, {})
1144
+ return {}
1145
+
1146
+ def run_doctests(self):
1147
+ """
1148
+ Actually run the doctests.
1149
+
1150
+ This function is called by :meth:`run`.
1151
+
1152
+ EXAMPLES::
1153
+
1154
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1155
+ sage: from sage.env import SAGE_SRC
1156
+ sage: import os
1157
+ sage: dirname = os.path.join(SAGE_SRC, 'sage', 'rings', 'homset.py')
1158
+ sage: DD = DocTestDefaults()
1159
+ sage: DC = DocTestController(DD, [dirname])
1160
+ sage: DC.expand_files_into_sources()
1161
+ sage: DC.run_doctests()
1162
+ Doctesting 1 file.
1163
+ sage -t .../sage/rings/homset.py
1164
+ [... tests, ...s wall]
1165
+ ----------------------------------------------------------------------
1166
+ All tests passed!
1167
+ ----------------------------------------------------------------------
1168
+ Total time for all tests: ... seconds
1169
+ cpu time: ... seconds
1170
+ cumulative wall time: ... seconds...
1171
+ """
1172
+ nfiles = 0
1173
+ nother = 0
1174
+ for F in self.sources:
1175
+ if isinstance(F, FileDocTestSource):
1176
+ nfiles += 1
1177
+ else:
1178
+ nother += 1
1179
+ if self.sources:
1180
+ filestr = ", ".join(([count_noun(nfiles, "file")] if nfiles else []) +
1181
+ ([count_noun(nother, "other source")] if nother else []))
1182
+ threads = " using %s threads" % (self.options.nthreads) if self.options.nthreads > 1 else ""
1183
+ iterations = []
1184
+ if self.options.global_iterations > 1:
1185
+ iterations.append("%s global iterations" % (self.options.global_iterations))
1186
+ if self.options.file_iterations > 1:
1187
+ iterations.append("%s file iterations" % (self.options.file_iterations))
1188
+ iterations = ", ".join(iterations)
1189
+ if iterations:
1190
+ iterations = " (%s)" % (iterations)
1191
+ if self.baseline_stats:
1192
+ self.log(f"Using --baseline-stats-path={self.options.baseline_stats_path}")
1193
+ self.log("Doctesting %s%s%s." % (filestr, threads, iterations))
1194
+ self.reporter = DocTestReporter(self)
1195
+ self.dispatcher = DocTestDispatcher(self)
1196
+ N = self.options.global_iterations
1197
+ for _ in range(N):
1198
+ try:
1199
+ self.timer = Timer().start()
1200
+ self.dispatcher.dispatch()
1201
+ except KeyboardInterrupt:
1202
+ break
1203
+ finally:
1204
+ self.timer.stop()
1205
+ self.reporter.finalize()
1206
+ self.cleanup(False)
1207
+ else:
1208
+ self.log("No files to doctest")
1209
+ self.reporter = DictAsObject({'error_status': 0, 'stats': {}})
1210
+
1211
+ def cleanup(self, final=True):
1212
+ """
1213
+ Run cleanup activities after actually running doctests.
1214
+
1215
+ In particular, saves the stats to disk and closes the logfile.
1216
+
1217
+ INPUT:
1218
+
1219
+ - ``final`` -- whether to close the logfile
1220
+
1221
+ EXAMPLES::
1222
+
1223
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1224
+ sage: from sage.env import SAGE_SRC
1225
+ sage: import os
1226
+ sage: dirname = os.path.join(SAGE_SRC, 'sage', 'rings', 'all.py')
1227
+ sage: DD = DocTestDefaults()
1228
+
1229
+ sage: DC = DocTestController(DD, [dirname])
1230
+ sage: DC.expand_files_into_sources()
1231
+ sage: DC.sources.sort(key=lambda s:s.basename)
1232
+
1233
+ sage: for i, source in enumerate(DC.sources):
1234
+ ....: DC.stats[source.basename] = {'walltime': 0.1r * (i+1)}
1235
+ ....:
1236
+
1237
+ sage: DC.run()
1238
+ Running doctests with ID ...
1239
+ Doctesting 1 file.
1240
+ sage -t .../rings/all.py
1241
+ [... tests, ...s wall]
1242
+ ----------------------------------------------------------------------
1243
+ All tests passed!
1244
+ ----------------------------------------------------------------------
1245
+ Total time for all tests: ... seconds
1246
+ cpu time: ... seconds
1247
+ cumulative wall time: ... seconds
1248
+ Features detected...
1249
+ 0
1250
+ sage: DC.cleanup()
1251
+ """
1252
+ self.stats.update(self.reporter.stats)
1253
+ self.save_stats(self.options.stats_path)
1254
+ # Close the logfile
1255
+ if final and self.logfile is not None:
1256
+ self.logfile.close()
1257
+ self.logfile = None
1258
+
1259
+ def _optional_tags_string(self):
1260
+ """
1261
+ Return a string describing the optional tags used.
1262
+
1263
+ OUTPUT: string with comma-separated tags (without spaces, so
1264
+ it can be used to build a command-line)
1265
+
1266
+ EXAMPLES::
1267
+
1268
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1269
+ sage: DC = DocTestController(DocTestDefaults(), [])
1270
+ sage: DC._optional_tags_string()
1271
+ 'sage'
1272
+ sage: DC = DocTestController(DocTestDefaults(optional='all,and,some,more'), [])
1273
+ sage: DC._optional_tags_string()
1274
+ 'all'
1275
+ sage: DC = DocTestController(DocTestDefaults(optional='sage,openssl'), [])
1276
+ sage: DC._optional_tags_string()
1277
+ 'openssl,sage'
1278
+ """
1279
+ tags = self.options.optional
1280
+ if tags is True:
1281
+ return "all"
1282
+ else:
1283
+ return ",".join(sorted(tags - auto_optional_tags))
1284
+
1285
+ def _assemble_cmd(self):
1286
+ """
1287
+ Assemble a shell command used in running tests under gdb, lldb, or valgrind.
1288
+
1289
+ EXAMPLES::
1290
+
1291
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1292
+ sage: DC = DocTestController(DocTestDefaults(timeout=123), ["hello_world.py"])
1293
+ sage: print(DC._assemble_cmd())
1294
+ ...python... -m sage.doctest --serial... --timeout=123... hello_world.py
1295
+ """
1296
+ cmd = f"{shlex.quote(sys.executable)} -m sage.doctest --serial "
1297
+ opt = dict_difference(self.options.__dict__, DocTestDefaults(runtest_default=True).__dict__)
1298
+ # Options with no argument
1299
+ for o in ("all", "installed", "long", "initial", "exitfirst",
1300
+ "force_lib", "if_installed", "abspath", "verbose",
1301
+ "debug", "only_errors", "failed", "new",
1302
+ "show_skipped"):
1303
+ if o in opt:
1304
+ cmd += "--%s " % o.replace('_', '-')
1305
+ # Options with one argument
1306
+ for o in ("timeout", "die_timeout", "logfile", "warn_long", "randorder",
1307
+ "random_seed", "global_iterations", "file_iterations",
1308
+ "environment", "baseline_stats_path", "stats_path"):
1309
+ if o in opt:
1310
+ cmd += "--%s=%s " % (o.replace('_', '-'), opt[o])
1311
+ # One with a different dest
1312
+ if "target_walltime" in opt:
1313
+ cmd += "--%s=%s " % ("short", opt[o])
1314
+ if "optional" in opt:
1315
+ cmd += "--optional={} ".format(self._optional_tags_string())
1316
+ return cmd + " ".join(self.files)
1317
+
1318
+ def run_val_gdb(self, testing=False):
1319
+ """
1320
+ Spawns a subprocess to run tests under the control of gdb, lldb, or valgrind.
1321
+
1322
+ INPUT:
1323
+
1324
+ - ``testing`` -- boolean (default: ``False``); if ``True`` then the
1325
+ command to be run will be printed rather than a subprocess started
1326
+
1327
+ EXAMPLES:
1328
+
1329
+ Note that the command lines include unexpanded environment
1330
+ variables. It is safer to let the shell expand them than to
1331
+ expand them here and risk insufficient quoting. ::
1332
+
1333
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1334
+ sage: DD = DocTestDefaults(gdb=True)
1335
+ sage: DC = DocTestController(DD, ["hello_world.py"])
1336
+ sage: DC.run_val_gdb(testing=True)
1337
+ exec gdb --eval-command="run" --args ...python... -m sage.doctest --serial... --timeout=0... hello_world.py
1338
+
1339
+ ::
1340
+
1341
+ sage: DD = DocTestDefaults(valgrind=True, optional='all', timeout=172800)
1342
+ sage: DC = DocTestController(DD, ["hello_world.py"])
1343
+ sage: DC.run_val_gdb(testing=True)
1344
+ exec valgrind --tool=memcheck --leak-resolution=high --leak-check=full --num-callers=25 --suppressions=.../valgrind/pyalloc.supp --suppressions=.../valgrind/sage.supp --suppressions=.../valgrind/sage-additional.supp --suppressions=.../valgrind/valgrind-python.supp --log-file=.../valgrind/sage-memcheck.%p ...python... -m sage.doctest --serial... --timeout=172800... --optional=all hello_world.py
1345
+ """
1346
+ try:
1347
+ sage_cmd = self._assemble_cmd()
1348
+ except ValueError:
1349
+ self.log(sys.exc_info()[1])
1350
+ return 2
1351
+ opt = self.options
1352
+
1353
+ if opt.gdb:
1354
+ cmd = f'''exec gdb --eval-command="run" --args '''
1355
+ flags = ""
1356
+ if opt.logfile:
1357
+ sage_cmd += f" --logfile {shlex.quote(opt.logfile)}"
1358
+ elif opt.lldb:
1359
+ cmd = f'''exec lldb --one-line "process launch" --one-line "cont" -- '''
1360
+ flags = ""
1361
+ else:
1362
+ if opt.logfile is None:
1363
+ default_log = os.path.join(DOT_SAGE, "valgrind")
1364
+ if os.path.exists(default_log):
1365
+ if not os.path.isdir(default_log):
1366
+ self.log(f"{default_log} must be a directory")
1367
+ return 2
1368
+ else:
1369
+ os.makedirs(default_log)
1370
+ logfile = os.path.join(default_log, "sage-%s")
1371
+ else:
1372
+ logfile = opt.logfile
1373
+ if opt.valgrind:
1374
+ toolname = "memcheck"
1375
+ flags = os.getenv("SAGE_MEMCHECK_FLAGS")
1376
+ if flags is None:
1377
+ flags = "--leak-resolution=high --leak-check=full --num-callers=25 "
1378
+ for supp in ["pyalloc.supp", "sage.supp", "sage-additional.supp", "valgrind-python.supp"]:
1379
+ fname = os.path.join(SAGE_EXTCODE, "valgrind", supp)
1380
+ flags += f"--suppressions={shlex.quote(fname)} "
1381
+ elif opt.massif:
1382
+ toolname = "massif"
1383
+ flags = os.getenv("SAGE_MASSIF_FLAGS", "--depth=6 ")
1384
+ elif opt.cachegrind:
1385
+ toolname = "cachegrind"
1386
+ flags = os.getenv("SAGE_CACHEGRIND_FLAGS", "")
1387
+ elif opt.omega:
1388
+ toolname = "exp-omega"
1389
+ flags = os.getenv("SAGE_OMEGA_FLAGS", "")
1390
+ cmd = "exec valgrind --tool=%s " % (toolname)
1391
+ flags += f''' --log-file={shlex.quote(logfile)} '''
1392
+ if opt.omega:
1393
+ toolname = "omega"
1394
+ if "%s" in flags:
1395
+ flags %= toolname + ".%p" # replace %s with toolname
1396
+ cmd += flags + sage_cmd
1397
+
1398
+ sys.stdout.flush()
1399
+ sys.stderr.flush()
1400
+ self.log(cmd)
1401
+
1402
+ if testing:
1403
+ return
1404
+
1405
+ # Setup signal handlers.
1406
+ # Save crash logs in temporary directory.
1407
+ os.putenv('CYSIGNALS_CRASH_LOGS', tmp_dir("crash_logs_"))
1408
+ init_cysignals()
1409
+
1410
+ import signal
1411
+ import subprocess
1412
+ p = subprocess.Popen(cmd, shell=True)
1413
+
1414
+ if opt.timeout > 0:
1415
+ signal.alarm(opt.timeout)
1416
+ try:
1417
+ return p.wait()
1418
+ except AlarmInterrupt:
1419
+ self.log(" Timed out")
1420
+ return 4
1421
+ except KeyboardInterrupt:
1422
+ self.log(" Interrupted")
1423
+ return 128
1424
+ finally:
1425
+ signal.alarm(0)
1426
+ if p.returncode is None:
1427
+ p.terminate()
1428
+
1429
+ def run(self):
1430
+ """
1431
+ This function is called after initialization to set up and run all doctests.
1432
+
1433
+ EXAMPLES::
1434
+
1435
+ sage: from sage.doctest.control import DocTestDefaults, DocTestController
1436
+ sage: from sage.env import SAGE_SRC
1437
+ sage: import os
1438
+ sage: DD = DocTestDefaults()
1439
+ sage: filename = os.path.join(SAGE_SRC, "sage", "sets", "non_negative_integers.py")
1440
+ sage: DC = DocTestController(DD, [filename])
1441
+ sage: DC.run()
1442
+ Running doctests with ID ...
1443
+ Doctesting 1 file.
1444
+ sage -t .../sage/sets/non_negative_integers.py
1445
+ [... tests, ...s wall]
1446
+ ----------------------------------------------------------------------
1447
+ All tests passed!
1448
+ ----------------------------------------------------------------------
1449
+ Total time for all tests: ... seconds
1450
+ cpu time: ... seconds
1451
+ cumulative wall time: ... seconds
1452
+ Features detected...
1453
+ 0
1454
+
1455
+ We check that :issue:`25378` is fixed (testing external packages
1456
+ while providing a logfile does not raise a ValueError: I/O
1457
+ operation on closed file)::
1458
+
1459
+ sage: logfile = tmp_filename(ext='.log')
1460
+ sage: DD = DocTestDefaults(optional=set(['sage', 'external']), logfile=logfile)
1461
+ sage: filename = tmp_filename(ext='.py')
1462
+ sage: DC = DocTestController(DD, [filename])
1463
+ sage: DC.run()
1464
+ Running doctests with ID ...
1465
+ Using --optional=external,sage
1466
+ Features to be detected: ...
1467
+ Doctesting 1 file.
1468
+ sage -t ....py
1469
+ [0 tests, ...s wall]
1470
+ ----------------------------------------------------------------------
1471
+ All tests passed!
1472
+ ----------------------------------------------------------------------
1473
+ Total time for all tests: ... seconds
1474
+ cpu time: ... seconds
1475
+ cumulative wall time: ... seconds
1476
+ Features detected...
1477
+ 0
1478
+
1479
+ We test the ``--hide`` option (:issue:`34185`)::
1480
+
1481
+ sage: from sage.doctest.control import test_hide
1482
+ sage: filename = tmp_filename(ext='.py')
1483
+ sage: with open(filename, 'w') as f:
1484
+ ....: f.write(test_hide)
1485
+ ....: f.close()
1486
+ 714
1487
+ sage: DF = DocTestDefaults(hide='buckygen,all')
1488
+ sage: DC = DocTestController(DF, [filename])
1489
+ sage: DC.run()
1490
+ Running doctests with ID ...
1491
+ Using --optional=sage...
1492
+ Features to be detected: ...
1493
+ Doctesting 1 file.
1494
+ sage -t ....py
1495
+ [4 tests, ...s wall]
1496
+ ----------------------------------------------------------------------
1497
+ All tests passed!
1498
+ ----------------------------------------------------------------------
1499
+ Total time for all tests: ... seconds
1500
+ cpu time: ... seconds
1501
+ cumulative wall time: ... seconds
1502
+ Features detected...
1503
+ 0
1504
+
1505
+ sage: DF = DocTestDefaults(hide='benzene,optional')
1506
+ sage: DC = DocTestController(DF, [filename])
1507
+ sage: DC.run()
1508
+ Running doctests with ID ...
1509
+ Using --optional=sage
1510
+ Features to be detected: ...
1511
+ Doctesting 1 file.
1512
+ sage -t ....py
1513
+ [4 tests, ...s wall]
1514
+ ----------------------------------------------------------------------
1515
+ All tests passed!
1516
+ ----------------------------------------------------------------------
1517
+ Total time for all tests: ... seconds
1518
+ cpu time: ... seconds
1519
+ cumulative wall time: ... seconds
1520
+ Features detected...
1521
+ 0
1522
+
1523
+ Test *Features that have been hidden* message::
1524
+
1525
+ sage: DC.run() # optional - meataxe
1526
+ Running doctests with ID ...
1527
+ Using --optional=sage
1528
+ Features to be detected: ...
1529
+ Doctesting 1 file.
1530
+ sage -t ....py
1531
+ [4 tests, ...s wall]
1532
+ ----------------------------------------------------------------------
1533
+ All tests passed!
1534
+ ----------------------------------------------------------------------
1535
+ Total time for all tests: ... seconds
1536
+ cpu time: ... seconds
1537
+ cumulative wall time: ... seconds
1538
+ Features detected...
1539
+ Features that have been hidden: ...meataxe...
1540
+ 0
1541
+ """
1542
+ opt = self.options
1543
+ L = (opt.gdb, opt.lldb, opt.valgrind, opt.massif, opt.cachegrind, opt.omega)
1544
+ if any(L):
1545
+ if L.count(True) > 1:
1546
+ self.log("You may only specify one of gdb, valgrind/memcheck, massif, cachegrind, omega")
1547
+ return 2
1548
+ return self.run_val_gdb()
1549
+ else:
1550
+ self.create_run_id()
1551
+ from sage.env import SAGE_ROOT_GIT, SAGE_LOCAL, SAGE_VENV
1552
+ # SAGE_ROOT_GIT can be None on distributions which typically
1553
+ # only have the SAGE_LOCAL install tree but not SAGE_ROOT
1554
+ if (SAGE_ROOT_GIT is not None) and os.path.isdir(SAGE_ROOT_GIT):
1555
+ import subprocess
1556
+ try:
1557
+ branch = subprocess.check_output(["git",
1558
+ "--git-dir=" + SAGE_ROOT_GIT,
1559
+ "rev-parse",
1560
+ "--abbrev-ref",
1561
+ "HEAD"])
1562
+ branch = branch.decode('utf-8')
1563
+ self.log("Git branch: " + branch, end="")
1564
+ except subprocess.CalledProcessError:
1565
+ pass
1566
+ try:
1567
+ ref = subprocess.check_output(["git",
1568
+ "--git-dir=" + SAGE_ROOT_GIT,
1569
+ "describe",
1570
+ "--always",
1571
+ "--dirty"])
1572
+ ref = ref.decode('utf-8')
1573
+ self.log("Git ref: " + ref, end="")
1574
+ except subprocess.CalledProcessError:
1575
+ pass
1576
+
1577
+ self.log(f"Running with {SAGE_LOCAL=} and {SAGE_VENV=}")
1578
+
1579
+ self.log("Using --optional=" + self._optional_tags_string())
1580
+ available_software._allow_external = self.options.optional is True or 'external' in self.options.optional
1581
+
1582
+ for h in self.options.hide:
1583
+ try:
1584
+ i = available_software._indices[h]
1585
+ except KeyError:
1586
+ pass
1587
+ else:
1588
+ f = available_software._features[i]
1589
+ f.hide()
1590
+ self.options.hidden_features.add(f)
1591
+ for g in f.joined_features():
1592
+ if g.name in self.options.optional:
1593
+ self.options.optional.discard(g.name)
1594
+
1595
+ for o in self.options.disabled_optional:
1596
+ try:
1597
+ i = available_software._indices[o]
1598
+ except KeyError:
1599
+ pass
1600
+ else:
1601
+ available_software._seen[i] = -1
1602
+
1603
+ self.log("Features to be detected: " + ','.join(available_software.detectable()))
1604
+ if self.options.probe:
1605
+ self.log("Features to be probed: " + ('all' if self.options.probe is True
1606
+ else ','.join(self.options.probe)))
1607
+ self.add_files()
1608
+ self.expand_files_into_sources()
1609
+ self.filter_sources()
1610
+ self.sort_sources()
1611
+ self.run_doctests()
1612
+
1613
+ self.log("Features detected for doctesting: "
1614
+ + ','.join(available_software.seen()))
1615
+ if self.options.hidden_features:
1616
+ for f in self.options.hidden_features:
1617
+ f.unhide()
1618
+ self.log("Features that have been hidden: " + ','.join(available_software.hidden()))
1619
+ self.cleanup()
1620
+ return self.reporter.error_status
1621
+
1622
+
1623
+ def run_doctests(module, options=None):
1624
+ """
1625
+ Run the doctests in a given file.
1626
+
1627
+ INPUT:
1628
+
1629
+ - ``module`` -- a Sage module, a string, or a list of such
1630
+
1631
+ - ``options`` -- a DocTestDefaults object or ``None``
1632
+
1633
+ EXAMPLES::
1634
+
1635
+ sage: run_doctests(sage.rings.all)
1636
+ Running doctests with ID ...
1637
+ Doctesting 1 file.
1638
+ sage -t .../sage/rings/all.py
1639
+ [... tests, ...s wall]
1640
+ ----------------------------------------------------------------------
1641
+ All tests passed!
1642
+ ----------------------------------------------------------------------
1643
+ Total time for all tests: ... seconds
1644
+ cpu time: ... seconds
1645
+ cumulative wall time: ... seconds
1646
+ Features detected...
1647
+ """
1648
+ import sys
1649
+ sys.stdout.flush()
1650
+
1651
+ def stringify(x):
1652
+ if isinstance(x, (list, tuple)):
1653
+ F = [stringify(a) for a in x]
1654
+ return sage.misc.flatten.flatten(F)
1655
+ elif isinstance(x, types.ModuleType):
1656
+ F = x.__file__.replace(SAGE_LIB, SAGE_SRC)
1657
+ base, pyfile = os.path.split(F)
1658
+ file, ext = os.path.splitext(pyfile)
1659
+ if ext == ".pyc":
1660
+ ext = ".py"
1661
+ elif ext == ".so":
1662
+ ext = ".pyx"
1663
+ if file == "__init__":
1664
+ return [base]
1665
+ else:
1666
+ return [os.path.join(base, file) + ext]
1667
+ elif isinstance(x, str):
1668
+ return [os.path.abspath(x)]
1669
+ F = stringify(module)
1670
+ if options is None:
1671
+ options = DocTestDefaults()
1672
+ DC = DocTestController(options, F)
1673
+
1674
+ # Determine whether we're in doctest mode
1675
+ save_dtmode = sage.doctest.DOCTEST_MODE
1676
+
1677
+ # We need the following if we're not in DOCTEST_MODE
1678
+ # Tell IPython to avoid colors: it screws up the output checking.
1679
+ if not save_dtmode:
1680
+ if options.debug:
1681
+ raise ValueError("You should not try to run doctests with a debugger from within Sage: IPython objects to embedded shells")
1682
+ from IPython.core.getipython import get_ipython
1683
+ IP = get_ipython()
1684
+ if IP is not None:
1685
+ old_color = IP.colors
1686
+ IP.run_line_magic('colors', 'NoColor')
1687
+ old_config_color = IP.config.TerminalInteractiveShell.colors
1688
+ IP.config.TerminalInteractiveShell.colors = 'NoColor'
1689
+
1690
+ try:
1691
+ DC.run()
1692
+ finally:
1693
+ sage.doctest.DOCTEST_MODE = save_dtmode
1694
+ if not save_dtmode and IP is not None:
1695
+ IP.run_line_magic('colors', old_color)
1696
+ IP.config.TerminalInteractiveShell.colors = old_config_color
1697
+
1698
+
1699
+ ###############################################################################
1700
+ # Declaration of doctest strings
1701
+ ###############################################################################
1702
+
1703
+ test_hide = r"""r{quotmark}
1704
+ {prompt}: next(graphs.fullerenes(20))
1705
+ Traceback (most recent call last):
1706
+ ...
1707
+ FeatureNotPresentError: buckygen is not available.
1708
+ ...
1709
+ {prompt}: next(graphs.fullerenes(20)) # optional - buckygen
1710
+ Graph on 20 vertices
1711
+
1712
+ {prompt}: len(list(graphs.fusenes(2)))
1713
+ Traceback (most recent call last):
1714
+ ...
1715
+ FeatureNotPresentError: benzene is not available.
1716
+ ...
1717
+ {prompt}: len(list(graphs.fusenes(2))) # optional - benzene
1718
+ 1
1719
+ {prompt}: from sage.matrix.matrix_space import get_matrix_class
1720
+ {prompt}: get_matrix_class(GF(25,'x'), 4, 4, False, 'meataxe')
1721
+ Failed lazy import:
1722
+ meataxe is not available.
1723
+ ...
1724
+ {prompt}: get_matrix_class(GF(25,'x'), 4, 4, False, 'meataxe') # optional - meataxe
1725
+ <class 'sage.matrix.matrix_gfpn_dense.Matrix_gfpn_dense'>
1726
+ {quotmark}
1727
+ """.format(quotmark='"""', prompt='sage') # using prompt to hide these lines from _test_enough_doctests