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,708 @@
1
+ #!python
2
+ """
3
+ Given the output of doctest and a file, adjust the doctests so they won't fail.
4
+
5
+ Doctest failures due to exceptions are ignored.
6
+
7
+ AUTHORS::
8
+
9
+ - Nicolas M. Thiéry <nthiery at users dot sf dot net> Initial version (2008?)
10
+
11
+ - Andrew Mathas <andrew dot mathas at sydney dot edu dot au> 2013-02-14
12
+ Cleaned up the code and hacked it so that the script can now cope with the
13
+ situations when either the expected output or computed output are empty.
14
+ Added doctest to sage.tests.cmdline
15
+ """
16
+
17
+ # ****************************************************************************
18
+ # Copyright (C) 2006 William Stein
19
+ # 2009 Nicolas M. Thiery
20
+ # 2013 Andrew Mathas
21
+ # 2014 Volker Braun
22
+ # 2020 Jonathan Kliem
23
+ # 2021 Frédéric Chapoton
24
+ # 2023 Matthias Koeppe
25
+ #
26
+ # Distributed under the terms of the GNU General Public License (GPL)
27
+ # as published by the Free Software Foundation; either version 2 of
28
+ # the License, or (at your option) any later version.
29
+ # https://www.gnu.org/licenses/
30
+ # ****************************************************************************
31
+
32
+ import itertools
33
+ import json
34
+ import os
35
+ import re
36
+ import shlex
37
+ import subprocess
38
+ import sys
39
+
40
+ from argparse import ArgumentParser
41
+ from collections import defaultdict
42
+ from pathlib import Path
43
+
44
+ from sage.doctest.control import DocTestDefaults, DocTestController
45
+ from sage.doctest.parsing import parse_file_optional_tags, parse_optional_tags, unparse_optional_tags, update_optional_tags
46
+ from sage.env import SAGE_ROOT
47
+ from sage.features import PythonModule
48
+ from sage.features.all import all_features, module_feature, name_feature
49
+ from sage.misc.cachefunc import cached_function
50
+ from sage.misc.temporary_file import tmp_filename
51
+
52
+ parser = ArgumentParser(description="Given an input file with doctests, this creates a modified file that passes the doctests (modulo any raised exceptions). By default, the input file is modified. You can also name an output file.")
53
+ parser.add_argument('-l', '--long', dest='long', action="store_true", default=False,
54
+ help="include tests tagged '# long time'")
55
+ parser.add_argument("--distribution", type=str, default=[], action='append',
56
+ help="distribution package to test, e.g., 'sagemath-graphs', 'sagemath-combinat[modules]'; sets defaults for --venv and --environment. This option can be repeated to test several distributions")
57
+ parser.add_argument("--fixed-point", default=False, action="store_true",
58
+ help="whether to repeat until stable")
59
+ parser.add_argument("--venv", type=str, default='',
60
+ help="directory name of a venv where 'sage -t' is to be run")
61
+ parser.add_argument("--environment", type=str, default='',
62
+ help="name of a module that provides the global environment for tests, e.g., 'sage.all__sagemath_modules'; implies --keep-both and --full-tracebacks")
63
+ parser.add_argument("--no-test", default=False, action="store_true",
64
+ help="do not run the doctester, only rewrite '# optional/needs' tags; implies --only-tags")
65
+ parser.add_argument("--full-tracebacks", default=False, action="store_true",
66
+ help="include full tracebacks rather than '...'")
67
+ parser.add_argument("--only-tags", default=False, action="store_true",
68
+ help="only add '# optional/needs' tags where needed, ignore other failures")
69
+ parser.add_argument("--probe", metavar="FEATURES", type=str, default='',
70
+ help="check whether '# optional/needs' tags are still needed, remove those not needed")
71
+ parser.add_argument("--keep-both", default=False, action="store_true",
72
+ help="do not replace test results; duplicate the test instead, showing both results, and mark both copies '# optional'")
73
+ parser.add_argument("--overwrite", default=False, action="store_true",
74
+ help="never interpret a second filename as OUTPUT; overwrite the source files")
75
+ parser.add_argument("--no-overwrite", default=False, action="store_true",
76
+ help="never interpret a second filename as OUTPUT; output goes to files named INPUT.fixed")
77
+ parser.add_argument("--update-known-test-failures", default=False, action="store_true",
78
+ help="update the file pkgs/DISTRIBUTION/known-test-failures.json")
79
+ parser.add_argument("--verbose", default=False, action="store_true",
80
+ help="show details of all changes; implies --no-diff")
81
+ parser.add_argument("--no-diff", default=False, action="store_true",
82
+ help="don't show the 'git diff' of the modified files")
83
+ parser.add_argument("filename", nargs='*', help="input filenames; or (deprecated) INPUT_FILENAME OUTPUT_FILENAME if exactly two filenames are given and neither --overwrite nor --no-overwrite is present",
84
+ type=str)
85
+
86
+ runtest_default_environment = "sage.repl.ipython_kernel.all_jupyter"
87
+
88
+ def plain_distribution_and_extras(distribution):
89
+ # shortcuts / variants
90
+ distribution = distribution.replace('_', '-')
91
+ if not (distribution.startswith('sagemath-')
92
+ or distribution.startswith('sage-')):
93
+ distribution = f'sagemath-{distribution}'
94
+ # extras
95
+ m = re.fullmatch(r'([^[]*)(\[([^]]*)\])?', distribution)
96
+ return m.group(1), m.group(3)
97
+
98
+ def default_venv_environment_from_distribution(distribution):
99
+ if distribution:
100
+ plain_distribution, extras = plain_distribution_and_extras(distribution)
101
+ tox_env_name = 'sagepython-sagewheels-nopypi-norequirements'
102
+ if extras:
103
+ tox_env_name += '-' + extras.replace(',', '-')
104
+ default_venv = os.path.join(SAGE_ROOT, 'pkgs', plain_distribution, '.tox', tox_env_name)
105
+ if plain_distribution == 'sagemath-standard-no-symbolics':
106
+ default_environment = 'sage.all'
107
+ else:
108
+ default_environment = 'sage.all__' + plain_distribution.replace('-', '_')
109
+ else:
110
+ default_venv = ''
111
+ default_environment = runtest_default_environment
112
+ return default_venv, default_environment
113
+
114
+
115
+ @cached_function
116
+ def venv_explainer(distribution, venv=None, environment=None):
117
+ venv_explainers = []
118
+ default_venv, default_environment = default_venv_environment_from_distribution(distribution)
119
+ if venv:
120
+ if m := re.search(f'pkgs/(sage[^/]*)/[.]tox/((sagepython|sagewheels|nopypi|norequirements)-*)*([^/]*)$',
121
+ venv):
122
+ distribution, extras = m.group(1), m.group(4)
123
+ if extras:
124
+ distribution += '[' + extras.replace('-', ',') + ']'
125
+ default_venv_given_distribution, default_environment_given_distribution = default_venv_environment_from_distribution(distribution)
126
+
127
+ if (Path(venv).resolve() == Path(default_venv_given_distribution).resolve()
128
+ or not environment or environment == default_environment_given_distribution):
129
+ venv_explainers.append(f'--distribution {shlex.quote(distribution)}')
130
+ default_venv, default_environment = default_venv_given_distribution, default_environment_given_distribution
131
+
132
+ if venv and Path(venv).resolve() != Path(default_venv).resolve():
133
+ venv_explainers.append(f'--venv {shlex.quote(venv)}')
134
+ if environment and environment != default_environment:
135
+ venv_explainers.append(f'--environment {environment}')
136
+
137
+ if venv_explainers:
138
+ return ' (with ' + ' '.join(venv_explainers) + ')'
139
+ return ''
140
+
141
+
142
+ sep = "**********************************************************************\n"
143
+
144
+
145
+ def process_block(block, src_in_lines, file_optional_tags, venv_explainer=''):
146
+ if args.verbose:
147
+ print(sep + block.rstrip())
148
+
149
+ # Extract the line, what was expected, and was got.
150
+ if not (m := re.match('File "([^"]*)", line ([0-9]+), in ', block)):
151
+ return
152
+
153
+ def print_line(num):
154
+ if args.verbose and (src := src_in_lines[num]):
155
+ if src:
156
+ for line in src.split('\n'):
157
+ line = line.strip()
158
+ if line.startswith("sage: ") or line.startswith("....: "):
159
+ line = line[6:]
160
+ print(f" {line}") # indent to match the displayed "Example" in the sage-runtest message
161
+
162
+ def update_line(num, new, message=None):
163
+ src_in_lines[num] = new
164
+ if args.verbose and message:
165
+ print(f"sage-fixdoctests: {message}")
166
+ print_line(num)
167
+
168
+ def append_to_line(num, new, message=None):
169
+ update_line(num, src_in_lines[num] + new, message=message)
170
+
171
+ def prepend_to_line(num, new, message=None):
172
+ update_line(num, new + src_in_lines[num], message=message)
173
+
174
+ def update_line_optional_tags(num, *args, message=None, **kwds):
175
+ update_line(num,
176
+ update_optional_tags(src_in_lines[num], *args, **kwds),
177
+ message=message)
178
+
179
+ filename = m.group(1)
180
+ first_line_num = line_num = int(m.group(2)) # 1-based line number of the first line of the example
181
+
182
+ if m := re.search(r"using.*block-scoped tag.*'(sage: .*)'.*to avoid repeating the tag", block):
183
+ indent = (len(src_in_lines[first_line_num - 1]) - len(src_in_lines[first_line_num - 1].lstrip()))
184
+ append_to_line(line_num - 2, '\n' + ' ' * indent + m.group(1),
185
+ message="Adding this block-scoped tag")
186
+ print_line(first_line_num - 1)
187
+
188
+ if m := re.search(r"updating.*block-scoped tag.*'sage: (.*)'.*to avoid repeating the tag", block):
189
+ update_line_optional_tags(first_line_num - 1, tags=parse_optional_tags('# ' + m.group(1)),
190
+ message="Adding this tag to the existing block-scoped tag")
191
+
192
+ if m := re.search(r"referenced here was set only in doctest marked '# (optional|needs)[-: ]*([^;']*)", block):
193
+ optional = m.group(2).split()
194
+ if src_in_lines[first_line_num - 1].strip() in ['"""', "'''"]:
195
+ # This happens due to a virtual doctest in src/sage/repl/user_globals.py
196
+ return
197
+ optional = set(optional) - set(file_optional_tags)
198
+ update_line_optional_tags(first_line_num - 1, add_tags=optional,
199
+ message=f"Adding the tag(s) {optional}")
200
+
201
+ if m := re.search(r"tag '# (optional|needs)[-: ]([^;']*)' may no longer be needed", block):
202
+ optional = m.group(2).split()
203
+ update_line_optional_tags(first_line_num - 1, remove_tags=optional,
204
+ message=f"Removing the tag(s) {optional}")
205
+
206
+ if m2 := re.search('(Expected:|Expected nothing|Exception raised:)\n', block):
207
+ m1 = re.search('Failed example:\n', block)
208
+ line_num += block[m1.end() : m2.start()].count('\n') - 1
209
+ # Now line_num is the 1-based line number of the last line of the example
210
+
211
+ if m2.group(1) == 'Expected nothing':
212
+ expected = ''
213
+ block = '\n' + block[m2.end():] # so that split('\nGot:\n') does not fail below
214
+ elif m2.group(1) == 'Exception raised:':
215
+ # In this case, the doctester does not show the expected output,
216
+ # so we do not know how many lines it spans; so we check for the next prompt or
217
+ # docstring end.
218
+ expected = []
219
+ indentation = ' ' * (len(src_in_lines[line_num - 1]) - len(src_in_lines[line_num - 1].lstrip()))
220
+ i = line_num
221
+ while ((not src_in_lines[i].rstrip() or src_in_lines[i].startswith(indentation))
222
+ and not re.match(' *(sage:|""")', src_in_lines[i])):
223
+ expected.append(src_in_lines[i])
224
+ i += 1
225
+ block = '\n'.join(expected) + '\nGot:\n' + block[m2.end():]
226
+ else:
227
+ block = block[m2.end():]
228
+ else:
229
+ return
230
+
231
+ # Error testing.
232
+ if m := re.search(r"(?:ModuleNotFoundError: No module named|ImportError: cannot import name '(.*?)' from) '(.*?)'|AttributeError: module '(.*)?' has no attribute '(.*?)'", block):
233
+ if m.group(1):
234
+ # "ImportError: cannot import name 'function_field_polymod' from 'sage.rings.function_field' (unknown location)"
235
+ module = m.group(2) + '.' + m.group(1)
236
+ elif m.group(2):
237
+ # "ModuleNotFoundError: No module named ..."
238
+ module = m.group(2)
239
+ else:
240
+ # AttributeError: module 'sage.rings' has no attribute 'qqbar'
241
+ module = m.group(3) + '.' + m.group(4)
242
+ asked_why = re.search('#.*(why|explain)', src_in_lines[first_line_num - 1])
243
+ optional = module_feature(module)
244
+ if optional and optional.name not in file_optional_tags:
245
+ update_line_optional_tags(first_line_num - 1, add_tags=[optional.name],
246
+ message=f"Module '{module}' may be provided by feature '{optional.name}'; adding this tag")
247
+ if not asked_why:
248
+ # When no explanation has been demanded,
249
+ # we just mark the doctest with the feature
250
+ return
251
+ # Otherwise, continue and show the backtrace as 'GOT'
252
+
253
+ if 'Traceback (most recent call last):' in block:
254
+
255
+ expected, got = block.split('\nGot:\n')
256
+ if args.full_tracebacks:
257
+ if re.fullmatch(' *\n', got):
258
+ got = got[re.end(0):]
259
+ # don't show doctester internals (anything before first "<doctest...>" frame
260
+ if m := re.search('( *Traceback.*\n *)(?s:.*?)(^ *File "<doctest)( [^>]*)>', got, re.MULTILINE):
261
+ got = m.group(1) + '...\n' + m.group(2) + '...' + got[m.end(3):]
262
+ while m := re.search(' *File "<doctest( [^>]*)>', got):
263
+ got = got[:m.start(1)] + '...' + got[m.end(1):]
264
+ # simplify filenames shown in backtrace
265
+ while m := re.search('"([-a-zA-Z0-9._/]*/site-packages)/sage/', got):
266
+ got = got[:m.start(1)] + '...' + got[m.end(1):]
267
+
268
+ last_frame = got.rfind('File "')
269
+ if (last_frame >= 0
270
+ and (index_NameError := got.rfind("NameError:")) >= 0
271
+ and got[last_frame:].startswith('File "<doctest')):
272
+ if args.verbose:
273
+ print("sage-fixdoctests: This is a NameError from the top level of the doctest") # so we keep it brief
274
+ if m := re.match("NameError: name '(.*)'", got[index_NameError:]):
275
+ name = m.group(1)
276
+ if name in ['I', 'i']:
277
+ add_tags = ['sage.symbolic'] # This is how we mark it currently (2023-08)
278
+ elif len(name) >= 2 and (feature := name_feature(name)) and feature.name != 'sage.all':
279
+ # Don't mark use of 'x' '# needs sage.symbolic'; that's almost always wrong
280
+ # Likewise for variables like 'R', 'r'
281
+ add_tags = [feature.name] # FIXME: This feature may actually already be present in line, block, or file. Move this lookup code into the doctester and issue more specific instructions
282
+ elif args.only_tags:
283
+ if args.verbose:
284
+ print("sage-fixdoctests: No feature providing this global is known; no action because of --only-tags")
285
+ return
286
+ else:
287
+ add_tags = [f"NameError ('{name}', {venv_explainer.lstrip().lstrip('(')}"]
288
+ else:
289
+ if args.only_tags:
290
+ if args.verbose:
291
+ print("sage-fixdoctests: No feature providing this global is known; no action because of --only-tags")
292
+ return
293
+ add_tags = [f"NameError{venv_explainer}"]
294
+ update_line_optional_tags(first_line_num - 1, add_tags=add_tags,
295
+ message=f"Adding tag {add_tags}")
296
+ return
297
+ got = got.splitlines()
298
+ else:
299
+ got = got.splitlines()
300
+ got = ['Traceback (most recent call last):', '...', got[-1].lstrip()]
301
+ elif block[-21:] == 'Got:\n <BLANKLINE>\n':
302
+ expected = block[:-22]
303
+ got = ['']
304
+ else:
305
+ expected, got = block.split('\nGot:\n')
306
+ got = re.sub(r'(doctest:warning).*^( *DeprecationWarning:)',
307
+ r'\1...\n\2',
308
+ got, 1, re.DOTALL | re.MULTILINE)
309
+ got = got.splitlines() # got can't be the empty string
310
+
311
+ if args.only_tags:
312
+ if args.verbose:
313
+ print("sage-fixdoctests: No action because of --only-tags")
314
+ return
315
+
316
+ expected = expected.splitlines()
317
+
318
+ if args.keep_both:
319
+ test_lines = ([update_optional_tags(src_in_lines[first_line_num - 1],
320
+ add_tags=[f'GOT{venv_explainer}'])]
321
+ + src_in_lines[first_line_num : line_num])
322
+ update_line_optional_tags(first_line_num - 1, add_tags=['EXPECTED'],
323
+ message="Marking the doctest with idempotent tag EXPECTED, creating another copy with idempotent tag GOT")
324
+ indent = (len(src_in_lines[line_num - 1]) - len(src_in_lines[line_num - 1].lstrip()))
325
+ line_num += len(expected) # skip to the last line of the expected output
326
+ append_to_line(line_num - 1, '\n'.join([''] + test_lines)) # 2nd copy of the test
327
+ # now line_num is the last line of the 2nd copy of the test
328
+ expected = []
329
+
330
+ # If we expected nothing, and got something, then we need to insert the line before line_num
331
+ # and match indentation with line number line_num-1
332
+ if not expected:
333
+ indent = (len(src_in_lines[first_line_num - 1]) - len(src_in_lines[first_line_num - 1].lstrip()))
334
+ append_to_line(line_num - 1,
335
+ '\n' + '\n'.join('%s%s' % (' ' * indent, line.lstrip()) for line in got),
336
+ message="Adding the new output")
337
+ return
338
+
339
+ # Guess how much extra indenting ``got`` needs to match with the indentation
340
+ # of src_in_lines - we match the indentation with the line in ``got`` which
341
+ # has the smallest indentation after lstrip(). Note that the amount of indentation
342
+ # required could be negative if the ``got`` block is indented. In this case
343
+ # ``indent`` is set to zero.
344
+ indent = max(0, (len(src_in_lines[line_num]) - len(src_in_lines[line_num].lstrip())
345
+ - min(len(got[j]) - len(got[j].lstrip()) for j in range(len(got)))))
346
+
347
+ # Double check that what was expected was indeed in the source file and if
348
+ # it is not then then print a warning for the user which contains the
349
+ # problematic lines.
350
+ if any(expected[i].strip() != src_in_lines[line_num + i].strip()
351
+ for i in range(len(expected))):
352
+ import warnings
353
+ txt = "Did not manage to replace\n%s\n%s\n%s\nwith\n%s\n%s\n%s"
354
+ warnings.warn(txt % ('>' * 40, '\n'.join(expected), '>' * 40,
355
+ '<' * 40, '\n'.join(got), '<' * 40))
356
+ return
357
+
358
+ # If we got nothing when we expected something then we delete the line from the
359
+ # output, otherwise, add all of what we `got` onto the end of src_in_lines[line_num]
360
+ if got == ['']:
361
+ update_line(line_num, None,
362
+ message="Expected something, got nothing; deleting the old output")
363
+ else:
364
+ update_line(line_num, '\n'.join((' ' * indent + got[i]) for i in range(len(got))),
365
+ message="Replacing the old expected output with the new output")
366
+
367
+ # Mark any remaining `expected` lines as ``None`` so as to preserve the line numbering
368
+ for i in range(1, len(expected)):
369
+ update_line(line_num + i, None)
370
+
371
+
372
+ # set input and output files
373
+ def output_filename(filename):
374
+ if len(args.filename) == 2 and not args.overwrite and not args.no_overwrite:
375
+ if args.filename[0] == filename:
376
+ return args.filename[1]
377
+ else:
378
+ return None
379
+ return filename + ".fixed"
380
+ if args.no_overwrite:
381
+ return filename + ".fixed"
382
+ return filename
383
+
384
+
385
+ tested_doctesters = set()
386
+ venv_files = {} # distribution -> files that are not yet known to be fixed points in venv; we add and remove items
387
+ venv_ignored_files = {} # distribution -> files that should be ignored; we only add items
388
+ unprocessed_files = set()
389
+
390
+
391
+ class BadDistribution(Exception):
392
+ pass
393
+
394
+
395
+ def doctest_blocks(args, input_filenames, distribution=None, venv=None, environment=None):
396
+ executable = f'{os.path.relpath(venv)}/bin/sage' if venv else 'sage'
397
+ environment_args = f'--environment {environment} ' if environment and environment != runtest_default_environment else ''
398
+ long_args = f'--long ' if args.long else ''
399
+ probe_args = f'--probe {shlex.quote(args.probe)} ' if args.probe else ''
400
+ lib_args = f'--if-installed ' if venv else ''
401
+ doc_file = tmp_filename()
402
+ if venv or environment_args:
403
+ # Test the doctester, putting the output of the test into sage's temporary directory
404
+ input = os.path.join(os.path.relpath(SAGE_ROOT), 'src', 'sage', 'version.py')
405
+ cmdline = f'{shlex.quote(executable)} -t {environment_args}{long_args}{probe_args}'.rstrip()
406
+ if cmdline not in tested_doctesters:
407
+ if args.verbose:
408
+ print(f'sage-fixdoctests: Checking whether the doctester "{cmdline}" works')
409
+ cmdline += f' {shlex.quote(input)}'
410
+ if status := os.waitstatus_to_exitcode(os.system(f'{cmdline} > {shlex.quote(doc_file)}')):
411
+ raise BadDistribution(f"Doctester exited with error status {status}")
412
+ tested_doctesters.add(cmdline)
413
+ # Run the doctester, putting the output of the test into sage's temporary directory
414
+ input_args = " ".join(shlex.quote(f) for f in input_filenames)
415
+ cmdline = f'{shlex.quote(executable)} -t -p {environment_args}{long_args}{probe_args}{lib_args}{input_args}'
416
+ print(f'Running "{cmdline}"')
417
+ os.system(f'{cmdline} > {shlex.quote(doc_file)}')
418
+
419
+ with open(doc_file, 'r') as doc:
420
+ doc_out = doc.read()
421
+
422
+ # Remove skipped files, echo control messages
423
+ for m in re.finditer(r"^Skipping '(.*?)'.*$", doc_out, re.MULTILINE):
424
+ print('sage-runtests: ' + m.group(0))
425
+ if distribution is not None:
426
+ venv_files[distribution].discard(m.group(1))
427
+ venv_ignored_files[distribution].add(m.group(1))
428
+
429
+ return doc_out.split(sep)
430
+
431
+
432
+ def block_filename(block):
433
+ if not (m := re.match('File "([^"]*)", line ([0-9]+), in ', block)):
434
+ return None
435
+ return m.group(1)
436
+
437
+
438
+ def expanded_filename_args():
439
+ DD = DocTestDefaults(optional='all', warn_long=10000)
440
+ DC = DocTestController(DD, input_filenames)
441
+ DC.add_files()
442
+ DC.expand_files_into_sources()
443
+ for source in DC.sources:
444
+ yield source.path
445
+
446
+
447
+ def process_grouped_blocks(grouped_iterator, distribution=None, venv=None, environment=None):
448
+
449
+ seen = set()
450
+
451
+ explainer = venv_explainer(distribution, venv, environment)
452
+
453
+ for input, blocks in grouped_iterator:
454
+
455
+ if not input: # Blocks of noise
456
+ continue
457
+ if input in seen:
458
+ continue
459
+ seen.add(input)
460
+
461
+ with open(input, 'r') as test_file:
462
+ src_in = test_file.read()
463
+ src_in_lines = src_in.splitlines()
464
+ shallow_copy_of_src_in_lines = list(src_in_lines)
465
+ file_optional_tags = set(parse_file_optional_tags(enumerate(src_in_lines)))
466
+ persistent_tags_counts = defaultdict(int)
467
+ tags_counts = defaultdict(int)
468
+
469
+ for block in blocks:
470
+ try:
471
+ process_block(block, src_in_lines, file_optional_tags, venv_explainer=explainer)
472
+ except Exception:
473
+ print('sage-fixdoctests: Failure to process block')
474
+ print(block)
475
+
476
+ # Now source line numbers do not matter any more, and lines can be real lines again
477
+ src_in_lines = list(itertools.chain.from_iterable(
478
+ [] if line is None else [''] if not line else line.splitlines()
479
+ for line in src_in_lines))
480
+
481
+ # Remove duplicate optional tags and rewrite all '# optional' that should be '# needs'
482
+ persistent_optional_tags = {}
483
+ persistent_optional_tags_counted = False
484
+ for i, line in enumerate(src_in_lines):
485
+ if m := re.match(' *sage: *(.*)#', line):
486
+ tags, line_sans_tags, is_persistent = parse_optional_tags(line, return_string_sans_tags=True)
487
+ if is_persistent:
488
+ persistent_optional_tags = {tag: explanation
489
+ for tag, explanation in tags.items()
490
+ if explanation or tag not in file_optional_tags}
491
+ persistent_optional_tags_counted = False
492
+ line = update_optional_tags(line, tags=persistent_optional_tags, force_rewrite='standard')
493
+ if re.fullmatch(' *sage: *', line):
494
+ # persistent (block-scoped or file-scoped) tag was removed, so remove the whole line
495
+ line = None
496
+ else:
497
+ tags = {tag: explanation
498
+ for tag, explanation in tags.items()
499
+ if explanation or (tag not in file_optional_tags
500
+ and tag not in persistent_optional_tags)}
501
+ line = update_optional_tags(line, tags=tags, force_rewrite='standard')
502
+ if not persistent_optional_tags_counted:
503
+ persistent_tags_counts[frozenset(persistent_optional_tags)] += 1
504
+ persistent_optional_tags_counted = True
505
+ tags_counts[frozenset(tags)] += 1
506
+ src_in_lines[i] = line
507
+ elif line.strip() in ['', '"""', "'''"]: # Blank line or end of docstring
508
+ persistent_optional_tags = {}
509
+ persistent_optional_tags_counted = False
510
+ elif re.match(' *sage: ', line):
511
+ if not persistent_optional_tags_counted:
512
+ persistent_tags_counts[frozenset(persistent_optional_tags)] += 1
513
+ persistent_optional_tags_counted = True
514
+ tags_counts[frozenset()] += 1
515
+
516
+ if src_in_lines != shallow_copy_of_src_in_lines:
517
+ if (output := output_filename(input)) is None:
518
+ print(f"sage-fixdoctests: Not saving modifications made in '{input}'")
519
+ else:
520
+ with open(output, 'w') as test_output:
521
+ for line in src_in_lines:
522
+ if line is None:
523
+ continue
524
+ test_output.write(line)
525
+ test_output.write('\n')
526
+ # Show summary of changes
527
+ if input != output:
528
+ print("sage-fixdoctests: The fixed doctests have been saved as '{0}'.".format(output))
529
+ else:
530
+ relative = os.path.relpath(output, SAGE_ROOT)
531
+ print(f"sage-fixdoctests: The input file '{output}' has been overwritten.")
532
+ if not args.no_diff and not relative.startswith('..'):
533
+ subprocess.call(['git', '--no-pager', 'diff', relative], cwd=SAGE_ROOT)
534
+ for other_distribution, file_set in venv_files.items():
535
+ if input not in venv_ignored_files[other_distribution]:
536
+ file_set.add(input)
537
+ else:
538
+ print(f"sage-fixdoctests: No fixes made in '{input}'")
539
+ if distribution is not None:
540
+ venv_files[distribution].discard(input)
541
+
542
+ unprocessed_files.discard(input)
543
+
544
+ if args.verbose:
545
+ if file_optional_tags:
546
+ print(f"File tags: ")
547
+ print(f" {' '.join(sorted(file_optional_tags))}")
548
+ if persistent_tags_counts:
549
+ print(f"Doctest blocks by persistent tags: ")
550
+ for tags, count in sorted(persistent_tags_counts.items(),
551
+ key=lambda i: i[1], reverse=True):
552
+ print(f"{count:6} {' '.join(sorted(tags)) or '(untagged)'}")
553
+ if tags_counts:
554
+ print(f"Doctest examples by tags: ")
555
+ for tags, count in sorted(tags_counts.items(),
556
+ key=lambda i: i[1], reverse=True):
557
+ print(f"{count:6} {' '.join(sorted(tags)) or '(untagged)'}")
558
+
559
+
560
+ def fix_with_distribution(file_set, distribution=None, verbose=False):
561
+ if verbose:
562
+ print("#" * 78)
563
+ print(f"sage-fixdoctests: Fixing with --distribution={shlex.quote(distribution)}")
564
+ default_venv, default_environment = default_venv_environment_from_distribution(distribution)
565
+ venv = args.venv or default_venv
566
+ environment = args.environment or default_environment
567
+ file_set_to_process = sorted(file_set)
568
+ file_set.clear()
569
+ try:
570
+ doctests = doctest_blocks(args, file_set_to_process,
571
+ distribution=distribution, venv=venv, environment=environment)
572
+ process_grouped_blocks(itertools.groupby(doctests, block_filename), # modifies file_set
573
+ distribution=distribution, venv=venv, environment=environment)
574
+ except BadDistribution as e:
575
+ if args.ignore_bad_distributions:
576
+ print(f"sage-fixdoctests: {e}, ignoring")
577
+ else:
578
+ sys.exit(f"sage-fixdoctests: {e}")
579
+
580
+
581
+ if __name__ == "__main__":
582
+
583
+ args = parser.parse_args()
584
+
585
+ if args.verbose:
586
+ args.no_diff = True
587
+
588
+ args.ignore_bad_distributions = False # This could also be a switch
589
+
590
+ args.update_failures_distribution = args.distribution
591
+
592
+ if args.distribution == ['all']:
593
+ args.distribution = ['sagemath-categories',
594
+ 'sagemath-modules',
595
+ 'sagemath-pari',
596
+ 'sagemath-graphs', 'sagemath-graphs[modules]', 'sagemath-graphs[modules,pari]',
597
+ 'sagemath-groups',
598
+ 'sagemath-combinat', 'sagemath-combinat[graphs]', 'sagemath-combinat[modules]',
599
+ 'sagemath-polyhedra', 'sagemath-polyhedra[standard]',
600
+ 'sagemath-schemes', 'sagemath-schemes[ntl]', 'sagemath-schemes[pari]',
601
+ 'sagemath-symbolics',
602
+ ''] # monolithic distribution
603
+ args.update_failures_distribution = args.distribution + ['sagemath-repl', # not included above because it knows too little and complains too much
604
+ 'sagemath-bliss',
605
+ 'sagemath-coxeter3',
606
+ 'sagemath-flint',
607
+ 'sagemath-glpk',
608
+ 'sagemath-linbox',
609
+ 'sagemath-plot',
610
+ 'sagemath-standard-no-symbolics']
611
+
612
+ args.ignore_bad_distributions = True
613
+
614
+ if not args.filename:
615
+ if not args.update_known_test_failures:
616
+ sys.exit("sage-fixdoctests: At least one filename is required when --update-known-test-failures is not used")
617
+ if not args.distribution:
618
+ sys.exit("sage-fixdoctests: At least one --distribution argument is required for --update-known-test-failures")
619
+
620
+ if args.distribution or args.venv or args.environment:
621
+ args.keep_both = args.full_tracebacks = True
622
+
623
+ if len(args.distribution) > 1:
624
+ if args.venv or args.environment:
625
+ sys.exit("sage-fixdoctests: at most one --distribution argument can be combined with --venv and --environment")
626
+ elif not args.distribution:
627
+ args.distribution = ['']
628
+
629
+ if len(args.filename) == 2 and not args.overwrite and not args.no_overwrite:
630
+ print("sage-fixdoctests: When passing two filenames, the second one is taken as an output filename; "
631
+ "this is deprecated. To pass two input filenames, use the option --overwrite.")
632
+ input_filenames = [args.filename[0]]
633
+ else:
634
+ input_filenames = args.filename
635
+
636
+ try:
637
+ unprocessed_files = set(expanded_filename_args())
638
+ for distribution in args.distribution:
639
+ venv_files[distribution] = set(unprocessed_files) # make copies
640
+ venv_ignored_files[distribution] = set()
641
+ if args.no_test:
642
+ pass
643
+ elif len(args.distribution) == 1 and not args.fixed_point:
644
+ fix_with_distribution(set(unprocessed_files), args.distribution[0])
645
+ else:
646
+ for distribution, file_set in venv_files.items():
647
+ fix_with_distribution(file_set, distribution, verbose=True)
648
+ if args.fixed_point:
649
+ if args.probe:
650
+ print(f"sage-fixdoctests: Turning off --probe for the following iterations")
651
+ # This forces convergence to a fixed point
652
+ args.probe = ''
653
+ while True:
654
+ # Run a distribution with largest number of files remaining to be checked
655
+ # because of the startup overhead of sage-runtests
656
+ distribution, file_set = max(venv_files.items(), key=lambda df: len(df[1]))
657
+ if not file_set:
658
+ break
659
+ while file_set:
660
+ fix_with_distribution(file_set, distribution, verbose=True)
661
+ # Immediately re-run with the same distribution to continue chains of
662
+ # "NameError" / "variable was set only in doctest" fixes
663
+
664
+ # Each file must be processed by process_grouped_blocks at least once to clean up tags,
665
+ # even if sage-runtest does not have any complaints.
666
+ if unprocessed_files:
667
+ print(f"sage-fixdoctests: Processing unprocessed files")
668
+ process_grouped_blocks([(filename, [])
669
+ for filename in unprocessed_files])
670
+
671
+ if args.fixed_point:
672
+ print(f"sage-fixdoctests: Fixed point reached")
673
+
674
+ if args.update_known_test_failures:
675
+ if args.update_failures_distribution == ['']:
676
+ print("sage-fixdoctests: Ignoring switch --update-known-test-failures because no --distribution was given")
677
+ else:
678
+ for distribution in sorted(args.update_failures_distribution):
679
+ if distribution == '':
680
+ continue
681
+ plain_distribution, extras = plain_distribution_and_extras(distribution)
682
+ default_venv, _ = default_venv_environment_from_distribution(distribution)
683
+ venv = args.venv or default_venv
684
+ try:
685
+ stats_filename = os.path.join(default_venv, '.sage/timings2.json')
686
+ with open(stats_filename, 'r') as stats_file:
687
+ stats = json.load(stats_file)
688
+ except FileNotFoundError:
689
+ print(f"sage-fixdoctests: {os.path.relpath(stats_filename, SAGE_ROOT)} "
690
+ "does not exist (ignoring)")
691
+ else:
692
+ for d in stats.values():
693
+ del d['walltime']
694
+ stats = {k: d for k, d in stats.items()
695
+ if d.get('failed') or d.get('ntests', True)}
696
+ if extras:
697
+ extras_suffix = '--' + '--'.join(extras.split(','))
698
+ else:
699
+ extras_suffix = ''
700
+ failures_file = os.path.join(SAGE_ROOT, 'pkgs', plain_distribution,
701
+ f'known-test-failures{extras_suffix}.json')
702
+ with open(failures_file, 'w') as f:
703
+ json.dump(stats, f, sort_keys=True, indent=4)
704
+ print(f"sage-fixdoctests: Updated {os.path.relpath(failures_file, SAGE_ROOT)}")
705
+
706
+ except Exception:
707
+ print(f"sage-fixdoctests: Internal error")
708
+ raise