passagemath-repl 10.5.1__py3-none-any.whl

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