pyDiffTools 0.1.8__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.
@@ -0,0 +1,747 @@
1
+ # PYTHON_ARGCOMPLETE_OK
2
+
3
+ import argparse
4
+ import sys
5
+ import os
6
+ import gzip
7
+ import time
8
+ import subprocess
9
+ import re
10
+ import nbformat
11
+ import difflib
12
+ import shutil
13
+ from pathlib import Path
14
+ from . import (
15
+ match_spaces,
16
+ split_conflict,
17
+ outline,
18
+ update_check,
19
+ )
20
+ from .continuous import cpb
21
+ from .wrap_sentences import wr as wrap_sentences_wr # registers wrap command
22
+ from .separate_comments import tex_sepcomments
23
+ from .unseparate_comments import tex_unsepcomments
24
+ from .comment_functions import matchingbrackets
25
+ from .copy_files import copy_image_files
26
+ from .searchacro import replace_acros
27
+ from .rearrange_tex import run as rearrange_tex_run
28
+ from .flowchart.watch_graph import wgrph
29
+ from .notebook.tex_to_qmd import tex2qmd
30
+ from .notebook.fast_build import qmdb, qmdinit
31
+
32
+ from .command_registry import _COMMAND_SPECS, register_command
33
+
34
+
35
+ def printed_exec(cmd):
36
+ print("about to execute:\n", cmd)
37
+ result = os.system(cmd)
38
+ if result != 0:
39
+ raise RuntimeError(
40
+ "os.system failed for command:\n"
41
+ + cmd
42
+ + "\n\nTry running the command by itself"
43
+ )
44
+
45
+
46
+ def errmsg():
47
+ parser = build_parser()
48
+ parser.print_help()
49
+ sys.exit(1)
50
+
51
+
52
+ _ROOT = os.path.abspath(os.path.dirname(__file__))
53
+
54
+
55
+ def get_data(path):
56
+ "return vbs and js scripts saved as package data"
57
+ return os.path.join(_ROOT, path)
58
+
59
+
60
+ def recursive_include_search(directory, basename, does_it_input):
61
+ with open(
62
+ os.path.join(directory, basename + ".tex"), "r", encoding="utf-8"
63
+ ) as fp:
64
+ alltxt = fp.read()
65
+ # we're only sensitive to the name of the file, not the directory that it's
66
+ # in
67
+ pattern = re.compile(
68
+ r"\n[^%]*\\(?:input|include)[{]((?:[^}]*/)?" + does_it_input + ")[}]"
69
+ )
70
+ for actual_name in pattern.findall(alltxt):
71
+ print(basename + " directly includes " + does_it_input)
72
+ return True, actual_name
73
+ print(
74
+ "file %s didn't directly include '%s' -- I'm going to look for the"
75
+ " files that it includes" % (basename, does_it_input)
76
+ )
77
+ pattern = re.compile(r"\n[^%]*\\(?:input|include)[{]([^}]+)[}](.*)")
78
+ for inputname, extra in pattern.findall(alltxt):
79
+ if "\\input" in extra or "\\include" in extra:
80
+ raise IOError(
81
+ "Don't put multiple include or input statements on one lien"
82
+ " --> are you trying to make my life difficult!!!??? "
83
+ )
84
+ print("%s includes input file:" % basename, inputname)
85
+ retval, actual_name = recursive_include_search(
86
+ directory, os.path.normpath(inputname), does_it_input
87
+ )
88
+ if retval:
89
+ return True, actual_name
90
+ return False, ""
91
+
92
+
93
+ def look_for_pdf(directory, origbasename):
94
+ """look for pdf -- if found return tuple(True, the basename of the pdf, the
95
+ basename of the tex) else return tuple(False, "", "")"""
96
+ found = False
97
+ basename = ""
98
+ actual_name = ""
99
+ for fname in os.listdir(directory):
100
+ if fname[-4:] == ".tex":
101
+ basename = fname[:-4]
102
+ print("found tex file", basename)
103
+ if os.path.exists(os.path.join(directory, basename + ".pdf")):
104
+ print("found matching tex/pdf pair", basename)
105
+ retval, actual_name = recursive_include_search(
106
+ directory, basename, origbasename
107
+ )
108
+ if retval:
109
+ return True, basename, actual_name
110
+ if not found:
111
+ print("but it doesn't seem to include", origbasename)
112
+ print("about to check for other inputs")
113
+ return found, basename, actual_name
114
+
115
+
116
+ @register_command(
117
+ "Make a python script from a notebook file, following certain rules."
118
+ )
119
+ def nb2py(arguments):
120
+ assert arguments[0].endswith(".ipynb"), (
121
+ "this is supposed to be called with a .ipynb file argument! (arguments"
122
+ " are %s)"
123
+ % repr(arguments)
124
+ )
125
+ nb = nbformat.read(arguments[0], nbformat.NO_CONVERT)
126
+ last_was_markdown = False
127
+ jupyter_magic_re = re.compile(r"%(.*)")
128
+ code_counter = 1
129
+ with open(
130
+ arguments[0].replace(".ipynb", ".py"), "w", encoding="utf-8"
131
+ ) as fpout:
132
+ for j in nb.cells:
133
+ lines = j["source"].split("\n")
134
+ if j["cell_type"] == "markdown":
135
+ for line in lines:
136
+ fpout.write("# " + line + "\n")
137
+ if len(lines[-1]) > 0:
138
+ # print "markdown, last is",repr(j['source'][-1])
139
+ fpout.write("\n")
140
+ last_was_markdown = True
141
+ elif j["cell_type"] == "code":
142
+ # fpout.write("start code\n")
143
+ if not last_was_markdown:
144
+ fpout.write("# In[%d]:\n\n" % code_counter)
145
+ code_counter += 1
146
+ for line in lines:
147
+ m = jupyter_magic_re.match(line)
148
+ if m:
149
+ fpout.write(
150
+ "get_ipython().magic(u'%s')\n" % m.groups()[0]
151
+ )
152
+ else:
153
+ fpout.write(line + "\n")
154
+ if len(lines[-1]) > 0:
155
+ # print "code, last is",repr(j['source'][-1])
156
+ fpout.write("\n")
157
+ last_was_markdown = False
158
+ # fpout.write("end code\n")
159
+ else:
160
+ raise ValueError("Unknown cell type")
161
+
162
+
163
+ @register_command(
164
+ "Make a notebook file from a python script, following certain rules."
165
+ )
166
+ def py2nb(arguments):
167
+ jupyter_magic_re = re.compile(
168
+ r"^get_ipython\(\).(?:run_line_)?magic\((?:u?['\"]([^'\"]*)['\"])?"
169
+ + 6 * r"(?:u?, *['\"]([^'\"]*)['\"])?"
170
+ + r"\)"
171
+ )
172
+ jupyter_cellmagic_re = re.compile(
173
+ r"^get_ipython\(\).run_cell_magic\((?:u?['\"]([^'\"]*)['\"])?"
174
+ + 6 * r"(?:u?, *['\"]([^'\"]*)['\"])?"
175
+ + r"\)\)"
176
+ )
177
+ assert len(arguments) == 1, "py2nb should only be called with one argument"
178
+ assert arguments[0].endswith(".py"), (
179
+ "this is supposed to be called with a .py file argument! (arguments"
180
+ " are %s)"
181
+ % repr(arguments)
182
+ )
183
+ with open(arguments[0], encoding="utf-8") as fpin:
184
+ text = fpin.read()
185
+ text = text.split("\n")
186
+ newtext = []
187
+ in_code_cell = False
188
+ last_line_empty = True
189
+ for thisline in text:
190
+ if thisline.startswith("#"):
191
+ if thisline.startswith("#!") and "python" in thisline:
192
+ pass
193
+ elif thisline.startswith("# coding: utf-8"):
194
+ pass
195
+ elif thisline.startswith("# In["):
196
+ in_code_cell = False
197
+ elif thisline.startswith("# Out["):
198
+ pass
199
+ elif thisline.startswith("# "):
200
+ # this is markdown only if the previous line was empty
201
+ if last_line_empty:
202
+ newtext.append("# <markdowncell>")
203
+ in_code_cell = False
204
+ newtext.append(thisline)
205
+ last_line_empty = False
206
+ elif len(thisline) == 0:
207
+ last_line_empty = True
208
+ newtext.append(thisline)
209
+ else:
210
+ if not in_code_cell:
211
+ newtext.append("# <codecell>")
212
+ in_code_cell = True
213
+ m = jupyter_magic_re.match(thisline)
214
+ if m:
215
+ thisline = "%" + " ".join(
216
+ (j for j in m.groups() if j is not None)
217
+ )
218
+ else:
219
+ m = jupyter_cellmagic_re.match(thisline)
220
+ if m:
221
+ thisline = "%%" + " ".join(
222
+ (j for j in m.groups() if j is not None)
223
+ )
224
+ newtext.append(thisline)
225
+ last_line_empty = False
226
+ text = "\n".join(newtext)
227
+
228
+ text += """
229
+ # <markdowncell>
230
+ # If you can read this, reads_py() is no longer broken!
231
+
232
+ """
233
+
234
+ nbook = nbformat.v3.reads_py(text)
235
+
236
+ nbook = nbformat.v4.upgrade(nbook) # Upgrade nbformat.v3 to nbformat.v4
237
+ nbook.metadata.update({
238
+ "kernelspec": {
239
+ "name": "Python [Anaconda2]",
240
+ "display_name": "Python [Anaconda2]",
241
+ "language": "python",
242
+ }
243
+ })
244
+
245
+ jsonform = nbformat.v4.writes(nbook) + "\n"
246
+ with open(
247
+ arguments[0].replace(".py", ".ipynb"), "w", encoding="utf-8"
248
+ ) as fpout:
249
+ fpout.write(jsonform)
250
+
251
+
252
+ @register_command(
253
+ "use a compiled latex original (first arg) to generate a synctex file for"
254
+ " a scanned document (second arg), e.g. with handwritten markup"
255
+ )
256
+ def gensync(arguments):
257
+ with gzip.open(arguments[0].replace(".pdf", ".synctek.gz")) as fp:
258
+ orig_synctex = fp.read()
259
+ fp.close()
260
+ # since the new synctex is in a new place, I need to tell it
261
+ # how to get back to the original place
262
+ relative_path = os.path.relpath(
263
+ os.path.dir(arguments[0]), os.path.dir(arguments[1])
264
+ )
265
+ base_fname = arguments[0].replace(".pdf", "")
266
+ new_synctex = orig_synctex.replace(base_fname, relative_path + base_fname)
267
+ new_synctex = orig_synctex
268
+ with gzip.open(arguments[1].replace(".pdf", ".synctek.gz")) as fp:
269
+ fp.write(new_synctex)
270
+ fp.write(arguments[1].replace())
271
+ fp.close()
272
+
273
+
274
+ @register_command("rearrange TeX file based on a .rrng plan")
275
+ def rrng(arguments):
276
+ rearrange_tex_run(arguments)
277
+
278
+
279
+ @register_command(
280
+ "git forward search, with arguments",
281
+ "git forward search, with arguments\n\n- file\n- line",
282
+ )
283
+ def gvr(arguments):
284
+ cmd = ["gvim"]
285
+ cmd.append("--remote-wait-silent")
286
+ cmd.append("+" + arguments[1])
287
+ cmd.append(arguments[0])
288
+ subprocess.Popen(" ".join(cmd)) # this is forked
289
+ time.sleep(0.3)
290
+ cmd = ["gvim"]
291
+ cmd.append("--remote-send")
292
+ cmd.append('"zO"')
293
+ time.sleep(0.3)
294
+ cmd = ["gvim"]
295
+ cmd.append("--remote-send")
296
+ cmd.append('":cd %:h\n"')
297
+ subprocess.Popen(" ".join(cmd))
298
+
299
+
300
+ @register_command("match whitespace")
301
+ def wmatch(arguments):
302
+ match_spaces.run(arguments)
303
+
304
+
305
+ @register_command("split conflict")
306
+ def sc(arguments):
307
+ split_conflict.run(arguments)
308
+
309
+
310
+ @register_command("word diff")
311
+ def wd(arguments):
312
+ if arguments[0].find("Temp") > 0:
313
+ # {{{ if it's a temporary file, I need to make a real copy to run
314
+ # pandoc on
315
+ fp = open(arguments[0], encoding="utf-8")
316
+ contents = fp.read()
317
+ fp.close()
318
+ fp = open(
319
+ arguments[1].replace(".md", "_old.md"), "w", encoding="utf-8"
320
+ )
321
+ fp.write(contents)
322
+ fp.close()
323
+ arguments[0] = arguments[1].replace(".md", "_old.md")
324
+ # }}}
325
+ word_files = [x.replace(".md", ".docx") for x in arguments[:2]]
326
+ local_dir = os.path.dirname(arguments[1])
327
+ print("local directory:", local_dir)
328
+ for j in range(2):
329
+ if arguments[0][-5:] == ".docx":
330
+ print(
331
+ "the first argument has a docx extension, so I'm bypassing the"
332
+ " pandoc step"
333
+ )
334
+ else:
335
+ cmd = ["pandoc"]
336
+ cmd += [arguments[j]]
337
+ cmd += ["--csl=edited-pmid-format.csl"]
338
+ cmd += ["--bibliography library_abbrev_utf8.bib"]
339
+ cmd += ["-s --smart"]
340
+ if len(arguments) > 2:
341
+ if arguments[2][-5:] == ".docx":
342
+ cmd += ["--reference-docx=" + arguments[2]]
343
+ else:
344
+ raise RuntimeError(
345
+ "if you pass three arguments to wd, then the third"
346
+ " must be a template for the word document"
347
+ )
348
+ elif os.path.isfile(local_dir + os.path.sep + "template.docx"):
349
+ # by default, use template.docx in the current directory
350
+ cmd += [
351
+ "--reference-docx="
352
+ + local_dir
353
+ + os.path.sep
354
+ + "template.docx"
355
+ ]
356
+ cmd += ["-o"]
357
+ cmd += [word_files[j]]
358
+ print("about to run", " ".join(cmd))
359
+ os.system(" ".join(cmd))
360
+ cmd = ["start"]
361
+ cmd += [get_data("diff-doc.js")]
362
+ print("word files are", word_files)
363
+ if word_files[0].find("C:") > -1:
364
+ cmd += [word_files[0]]
365
+ else:
366
+ cmd += [os.getcwd() + os.path.sep + word_files[0]]
367
+ cmd += [os.getcwd() + os.path.sep + word_files[1]]
368
+ print("about to run", " ".join(cmd))
369
+ os.system(" ".join(cmd))
370
+
371
+
372
+ @register_command("Reverse search")
373
+ def rs(arguments):
374
+ cmd = [
375
+ "gvim",
376
+ "--servername",
377
+ "GVIM",
378
+ "--remote",
379
+ f"+{arguments[0]} {arguments[1]}",
380
+ ]
381
+ os.system(" ".join(cmd))
382
+ cmd = ["wmctrl", "-a", "GVIM"]
383
+ os.system(" ".join(cmd))
384
+ time.sleep(0.5)
385
+ cmd = ["gvim", "--remote-send", "'<esc>zO:cd %:h'<enter>"]
386
+ os.system(" ".join(cmd))
387
+
388
+
389
+ @register_command(
390
+ "smart latex forward-search",
391
+ "smart latex forward-search\n"
392
+ "currently this works specifically for sumatra pdf located\n"
393
+ 'at "C:\\Program Files\\SumatraPDF\\SumatraPDF.exe",\n'
394
+ "but can easily be adapted based on os, etc.\n"
395
+ "Add the following line (or something like it) to your vimrc:\n"
396
+ 'map <c-F>s :cd %:h\\|sil !pydifft fs %:p <c-r>=line(".")<cr><cr>\n'
397
+ "it will map Cntrl-F s to a forward search.",
398
+ )
399
+ def fs(arguments):
400
+ texfile, lineno = arguments
401
+ texfile = os.path.normpath(os.path.abspath(texfile))
402
+ directory, texfile = texfile.rsplit(os.path.sep, 1)
403
+ assert texfile[-4:] == ".tex", "needs to be called .tex"
404
+ origbasename = texfile[:-4]
405
+ if os.name == "posix":
406
+ # linux
407
+ cmd = ["zathura --synctex-forward"]
408
+ assert shutil.which("zathura"), (
409
+ "first, install zathura, then set ~/.config/zathura/zathurarc"
410
+ "to include"
411
+ 'set synctex-editor-command "pydifft rs %{line} %{input}"'
412
+ )
413
+ else:
414
+ # windows
415
+ cmd = ["start sumatrapdf -reuse-instance"]
416
+ if os.path.exists(os.path.join(directory, origbasename + ".pdf")):
417
+ temp = os.path.join(directory, origbasename + ".pdf")
418
+ cmd.append(f"{lineno}:0:{texfile} {temp}")
419
+ tex_name = origbasename
420
+ else:
421
+ print("no pdf file for this guy, looking for one that has one")
422
+ found, basename, tex_name = look_for_pdf(directory, origbasename)
423
+ if not found:
424
+ while os.path.sep in directory and directory.lower()[-1] != ":":
425
+ build_nb = os.path.join(directory, "build_nb")
426
+ directory, _ = directory.rsplit(os.path.sep, 1)
427
+ if os.path.exists(build_nb):
428
+ print("looking for a build_nb subdirectory")
429
+ found, basename, tex_name = look_for_pdf(
430
+ build_nb, origbasename
431
+ )
432
+ if found:
433
+ directory = build_nb
434
+ break
435
+ print("looking one directory up, in ", directory)
436
+ found, basename, tex_name = look_for_pdf(
437
+ directory, origbasename
438
+ )
439
+ if found:
440
+ break
441
+ if not found:
442
+ raise IOError("This is not the PDF you are looking for!!!")
443
+ print("result:", directory, origbasename, found, basename, tex_name)
444
+ # file has been found, so add to the command
445
+ cmd.append(
446
+ f"{lineno}:0:{tex_name}.tex"
447
+ f" {os.path.join(directory, basename + '.pdf')}"
448
+ )
449
+ if os.name == "posix":
450
+ cmd.append("&")
451
+ else:
452
+ cmd.append("-forward-search")
453
+ cmd.append(tex_name + ".tex")
454
+ cmd.append("%s -fwdsearch-color ff0000" % lineno)
455
+ print("changing to directory", directory)
456
+ os.chdir(directory)
457
+ print("about to execute:\n\t", " ".join(cmd))
458
+ os.system(" ".join(cmd))
459
+ if os.name == "posix":
460
+ cmd = ["wmctrl", "-a", basename + ".pdf"]
461
+ os.system(" ".join(cmd))
462
+
463
+
464
+ @register_command("Convert xml to xlsx")
465
+ def xx(arguments):
466
+ format_codes = {
467
+ "csv": 6,
468
+ "xlsx": 51,
469
+ "xml": 46,
470
+ } # determined by microsoft vbs
471
+ cmd = ["start"]
472
+ cmd += [get_data("xml2xlsx.vbs")]
473
+ first_ext = arguments[0].split(".")[-1]
474
+ second_ext = arguments[1].split(".")[-1]
475
+ for j in arguments[0:2]:
476
+ if j.find("C:") > -1:
477
+ cmd += [j]
478
+ else:
479
+ cmd += [os.getcwd() + os.path.sep + j]
480
+ cmd += [str(format_codes[j]) for j in [first_ext, second_ext]]
481
+ print("about to run", " ".join(cmd))
482
+ os.system(" ".join(cmd))
483
+
484
+
485
+ @register_command("compare files, and rank by how well they compare")
486
+ def cmp(arguments):
487
+ target = arguments[0]
488
+ arguments = arguments[1:]
489
+ with open(target, encoding="utf-8") as fp:
490
+ base_txt = fp.read()
491
+ retval = {}
492
+ for j in arguments:
493
+ with open(j, encoding="utf-8") as fp:
494
+ retval[j] = difflib.SequenceMatcher(
495
+ None, base_txt, fp.read()
496
+ ).ratio()
497
+ print(
498
+ "\n".join(
499
+ str(v) + "-->" + str(k)
500
+ for k, v in sorted(
501
+ retval.items(), key=lambda item: item[1], reverse=True
502
+ )
503
+ )
504
+ )
505
+
506
+
507
+ @register_command("tex separate comments")
508
+ def sepc(arguments):
509
+ tex_sepcomments(arguments[0])
510
+
511
+
512
+ @register_command("tex unseparate comments")
513
+ def unsepc(arguments):
514
+ tex_unsepcomments(arguments[0])
515
+
516
+
517
+ @register_command("convert annotated TeX sources to DOCX via pandoc")
518
+ def tex2docx(arguments):
519
+ filename = arguments[0]
520
+ assert filename[-4:] == ".tex"
521
+ basename = filename[:-4]
522
+ with open("%s.tex" % basename, "r", encoding="utf-8") as fp:
523
+ content = fp.read()
524
+ comment_re = re.compile(r"\\pdfcomment([A-Z]+)\b")
525
+ thismatch = comment_re.search(
526
+ content
527
+ ) # match doesn't work with newlines, apparently
528
+ while thismatch:
529
+ a = thismatch.start()
530
+ b, c = matchingbrackets(content, a, "{")
531
+ content = (
532
+ content[:a]
533
+ + content[a + 1 : b]
534
+ + "("
535
+ + content[b + 1 : c]
536
+ + ")"
537
+ + content[c + 1 :]
538
+ )
539
+ thismatch = comment_re.search(content)
540
+ with open("%s_parencomments.tex" % basename, "w", encoding="utf-8") as fp:
541
+ fp.write(r"\renewcommand{\nts}[1]{\textbf{\textit{#1}}}" + "\n")
542
+ fp.write(content)
543
+ printed_exec(
544
+ "pandoc %s_parencomments.tex -f latex+latex_macros -o %s.md"
545
+ % ((basename,) * 2)
546
+ )
547
+ with open("%s.md" % basename, "r", encoding="utf-8") as fp:
548
+ content = fp.read()
549
+ thisid = 2
550
+ comment_re = re.compile(r"pdfcomment([A-Z]+)\(")
551
+ thismatch = comment_re.search(
552
+ content
553
+ ) # match doesn't work with newlines, apparently
554
+ while thismatch:
555
+ a = thismatch.start()
556
+ b, c = matchingbrackets(content, a, "(")
557
+ author = thismatch.groups()[0]
558
+ content = (
559
+ content[:a]
560
+ + '[%s]{.comment-start id="%d" author="%s"}'
561
+ % (content[b + 1 : c], thisid, author)
562
+ + '[]{.comment-end id="%d"}' % thisid
563
+ + content[c + 1 :]
564
+ )
565
+ thisid += 1
566
+ thismatch = comment_re.search(content)
567
+ with open("%s.md" % basename, "w", encoding="utf-8") as fp:
568
+ fp.write(content)
569
+ printed_exec("pandoc %s.md -o %s.docx" % ((basename,) * 2))
570
+ printed_exec("start %s.docx" % (basename))
571
+
572
+
573
+ @register_command("convert DOCX documents back to TeX while cleaning markup")
574
+ def docx2tex(arguments):
575
+ filename = arguments[0]
576
+ assert filename[-5:] == ".docx"
577
+ basename = filename[:-5]
578
+ printed_exec(
579
+ "pandoc %s.docx --track-changes=all -o %s.md" % ((basename,) * 2)
580
+ )
581
+ with open("%s.md" % basename, "r", encoding="utf-8") as fp:
582
+ content = fp.read()
583
+ citation_re = re.compile(r"\\\[\\@")
584
+ thismatch = citation_re.search(
585
+ content
586
+ ) # match doesn't work with newlines, apparently
587
+ while thismatch:
588
+ a, b = matchingbrackets(content, thismatch.start(), "[")
589
+ content = (
590
+ content[: a - 1] + content[a:b].replace("\\", "") + content[b:]
591
+ )
592
+ thismatch = citation_re.search(content)
593
+ with open("%s.md" % basename, "w", encoding="utf-8") as fp:
594
+ fp.write(content)
595
+ printed_exec(
596
+ "pandoc %s.md --biblatex -r markdown-auto_identifiers -o %s_reconv.tex"
597
+ % ((basename,) * 2)
598
+ )
599
+ print("about to match spaces:")
600
+ match_spaces.run((basename + ".tex", basename + "_reconv.tex"))
601
+ with open("%s_reconv.tex" % basename, "r", encoding="utf-8") as fp:
602
+ content = fp.read()
603
+ citation_re = re.compile(r"\\autocite\b")
604
+ content = citation_re.sub(r"\\cite", content)
605
+ paragraph_re = re.compile(r"\n\n(\\paragraph{.*)\n\n")
606
+ content = paragraph_re.sub(r"\1", content)
607
+ # {{{ convert \( to dollars
608
+ math_re = re.compile(r"\\\(")
609
+ thismatch = math_re.search(
610
+ content
611
+ ) # match doesn't work with newlines, apparently
612
+ while thismatch:
613
+ a = thismatch.start()
614
+ b, c = matchingbrackets(content, a, "(")
615
+ content = (
616
+ content[:a] + "$" + content[a + 1 : b] + "$" + content[c + 1 :]
617
+ )
618
+ thismatch = math_re.search(content)
619
+ # }}}
620
+ with open("%s_reconv.tex" % basename, "w", encoding="utf-8") as fp:
621
+ fp.write(content)
622
+
623
+
624
+ @register_command(
625
+ "make a gzip file suitable for arxiv (currently only test on linux)"
626
+ )
627
+ def arxiv(arguments):
628
+ ROOT_TEX = arguments[0].strip(".tex")
629
+ project_name = Path.cwd().name
630
+ TARGET_DIR = Path(f"../{project_name}_forarxiv/")
631
+ include_suppinfo = False
632
+ copy_image_files(ROOT_TEX, project_name, TARGET_DIR, include_suppinfo)
633
+ os.chdir(Path.cwd().parent)
634
+ output_filename = f"{project_name}_forarxiv.tgz"
635
+ # create tar process
636
+ tar = subprocess.Popen(
637
+ ["tar", "cf", "-", TARGET_DIR.name], stdout=subprocess.PIPE
638
+ )
639
+ # create gzip process, using tar's stdout as its stdin
640
+ gzip = subprocess.Popen(
641
+ ["gzip", "-9"], stdin=tar.stdout, stdout=subprocess.PIPE
642
+ )
643
+ # close tar's stdout so it doesn't hang around waiting for input
644
+ tar.stdout.close()
645
+ # write gzip's stdout to a file
646
+ with open(output_filename, "wb") as fp:
647
+ shutil.copyfileobj(gzip.stdout, fp)
648
+ gzip.stdout.close()
649
+
650
+
651
+ @register_command(
652
+ "look for the file myacronyms.sty (locally or in texmf) and use it"
653
+ " substitute your acronyms"
654
+ )
655
+ def ac(arguments):
656
+ # Run kpsewhich and capture the output
657
+ kpsewhich_output = subprocess.run(
658
+ ["kpsewhich", "myacronyms.sty"], capture_output=True, text=True
659
+ )
660
+
661
+ # Convert the string to a pathlib Path object if the file was found, else
662
+ # assign None
663
+ path = (
664
+ Path(kpsewhich_output.stdout.strip())
665
+ if kpsewhich_output.returncode == 0
666
+ else None
667
+ )
668
+ print("I'm using the acronyms in", path)
669
+ replace_acros(path)
670
+
671
+
672
+ @register_command(
673
+ "Paste mark down as formatted text into email, etc. (Tested on linux)"
674
+ )
675
+ def pmd(arguments):
676
+ # pandoc input.md -t html -o - | xclip -selection clipboard -t text/html
677
+ p1 = subprocess.Popen(
678
+ ["pandoc", arguments[0], "-t", "html", "-o", "-"],
679
+ stdout=subprocess.PIPE,
680
+ )
681
+ subprocess.run(
682
+ ["xclip", "-selection", "clipboard", "-t", "text/html"],
683
+ stdin=p1.stdout,
684
+ check=True,
685
+ )
686
+ p1.stdout.close()
687
+ p1.wait()
688
+
689
+
690
+ def build_parser():
691
+ parser = argparse.ArgumentParser()
692
+ subparsers = parser.add_subparsers(dest="command")
693
+ subparsers.required = True
694
+ for name, spec in _COMMAND_SPECS.items():
695
+ subparser = subparsers.add_parser(
696
+ name,
697
+ help=spec["help"],
698
+ description=spec["description"],
699
+ formatter_class=argparse.RawDescriptionHelpFormatter,
700
+ )
701
+ arguments = []
702
+ if "arguments" in spec:
703
+ arguments = spec["arguments"]
704
+ for argument in arguments:
705
+ flags = argument["flags"]
706
+ kwargs = dict(argument["kwargs"])
707
+ subparser.add_argument(*flags, **kwargs)
708
+ subparser.set_defaults(_handler=spec["handler"])
709
+ return parser
710
+
711
+
712
+ def main(argv=None):
713
+ if argv is None:
714
+ argv = sys.argv[1:]
715
+ # Run the PyPI update check once per UTC day so users see a notice but
716
+ # startup stays fast when offline. The date is stored in the
717
+ # PYDIFFTOOLS_UPDATE_CHECK_LAST_RAN_UTC_DATE environment variable.
718
+ today = time.strftime("%Y-%m-%d", time.gmtime())
719
+ already_checked_today = (
720
+ "PYDIFFTOOLS_UPDATE_CHECK_LAST_RAN_UTC_DATE" in os.environ
721
+ and os.environ["PYDIFFTOOLS_UPDATE_CHECK_LAST_RAN_UTC_DATE"] == today
722
+ )
723
+ if not already_checked_today:
724
+ os.environ["PYDIFFTOOLS_UPDATE_CHECK_LAST_RAN_UTC_DATE"] = today
725
+ current_version, latest_version, is_outdated = (
726
+ update_check.check_update("pyDiffTools")
727
+ )
728
+ if is_outdated and latest_version is not None:
729
+ print(
730
+ "A new pyDiffTools version is available "
731
+ f"(installed {current_version}, latest {latest_version}).",
732
+ file=sys.stderr,
733
+ )
734
+ parser = build_parser()
735
+ if not argv:
736
+ parser.print_help()
737
+ return
738
+ namespace = parser.parse_args(argv)
739
+ handler = namespace._handler
740
+ handler_kwargs = dict(vars(namespace))
741
+ handler_kwargs.pop("_handler", None)
742
+ handler_kwargs.pop("command", None)
743
+ handler(**handler_kwargs)
744
+
745
+
746
+ if __name__ == "__main__":
747
+ main()