pyDiffTools 0.1.25__tar.gz → 0.1.27__tar.gz
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.
- {pydifftools-0.1.25 → pydifftools-0.1.27}/MANIFEST.in +2 -0
- {pydifftools-0.1.25/pyDiffTools.egg-info → pydifftools-0.1.27}/PKG-INFO +5 -1
- {pydifftools-0.1.25 → pydifftools-0.1.27}/README.rst +4 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27/pyDiffTools.egg-info}/PKG-INFO +5 -1
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pyDiffTools.egg-info/SOURCES.txt +6 -0
- pydifftools-0.1.27/pydifftools/browser_lifecycle.py +68 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/command_line.py +80 -37
- pydifftools-0.1.27/pydifftools/command_registry.py +119 -0
- pydifftools-0.1.27/pydifftools/comment_tags_margin.lua +411 -0
- pydifftools-0.1.27/pydifftools/comment_tags_no_comments.lua +328 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/continuous.py +143 -98
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/flowchart/graph.py +151 -36
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/flowchart/watch_graph.py +165 -45
- pydifftools-0.1.27/pydifftools/git_gd.py +356 -0
- pydifftools-0.1.27/pydifftools/git_gd_qt.py +334 -0
- pydifftools-0.1.27/pydifftools/log_example.py +28 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/notebook/fast_build.py +228 -68
- pydifftools-0.1.27/pydifftools/update_check.py +73 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pyproject.toml +3 -1
- pydifftools-0.1.27/tests/test_command_line_help.py +70 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/tests/test_continuous_shutdown.py +1 -1
- {pydifftools-0.1.25 → pydifftools-0.1.27}/tests/test_update_check.py +48 -0
- pydifftools-0.1.25/pydifftools/browser_lifecycle.py +0 -25
- pydifftools-0.1.25/pydifftools/command_registry.py +0 -65
- pydifftools-0.1.25/pydifftools/update_check.py +0 -31
- {pydifftools-0.1.25 → pydifftools-0.1.27}/LICENSE.md +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/wrap_sentences.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/setup.cfg +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.27}/tests/test_tex_to_qmd.py +0 -0
|
@@ -5,5 +5,7 @@ include pydifftools/diff-doc.js
|
|
|
5
5
|
include pydifftools/xml2xlsx.vbs
|
|
6
6
|
include pydifftools/comments.css
|
|
7
7
|
include pydifftools/comment_tags.lua
|
|
8
|
+
include pydifftools/comment_tags_margin.lua
|
|
9
|
+
include pydifftools/comment_tags_no_comments.lua
|
|
8
10
|
include pydifftools/comment_toggle.js
|
|
9
11
|
recursive-include example_notebook *
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.27
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -106,6 +106,10 @@ included are (listed in order of fun/utility):
|
|
|
106
106
|
``_build``/``_display`` directories; with ``--watch`` it starts the HTTP
|
|
107
107
|
server and automatically rebuilds the staged fragments whenever you edit
|
|
108
108
|
a ``.qmd`` file.
|
|
109
|
+
- `pydifft gd [git diff args...]` shows the same Qt review table as the old
|
|
110
|
+
``git_gd_qt.py`` helper before launching ``git difftool`` for a selected
|
|
111
|
+
file. Run ``pydifft gd --install`` to add the matching ``git gd`` alias
|
|
112
|
+
to your global git config.
|
|
109
113
|
- `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
|
|
110
114
|
the bundled templates and example ``project1`` hierarchy, then downloads
|
|
111
115
|
MathJax into ``_template/mathjax`` so the builder can run immediately.
|
|
@@ -56,6 +56,10 @@ included are (listed in order of fun/utility):
|
|
|
56
56
|
``_build``/``_display`` directories; with ``--watch`` it starts the HTTP
|
|
57
57
|
server and automatically rebuilds the staged fragments whenever you edit
|
|
58
58
|
a ``.qmd`` file.
|
|
59
|
+
- `pydifft gd [git diff args...]` shows the same Qt review table as the old
|
|
60
|
+
``git_gd_qt.py`` helper before launching ``git difftool`` for a selected
|
|
61
|
+
file. Run ``pydifft gd --install`` to add the matching ``git gd`` alias
|
|
62
|
+
to your global git config.
|
|
59
63
|
- `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
|
|
60
64
|
the bundled templates and example ``project1`` hierarchy, then downloads
|
|
61
65
|
MathJax into ``_template/mathjax`` so the builder can run immediately.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.27
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -106,6 +106,10 @@ included are (listed in order of fun/utility):
|
|
|
106
106
|
``_build``/``_display`` directories; with ``--watch`` it starts the HTTP
|
|
107
107
|
server and automatically rebuilds the staged fragments whenever you edit
|
|
108
108
|
a ``.qmd`` file.
|
|
109
|
+
- `pydifft gd [git diff args...]` shows the same Qt review table as the old
|
|
110
|
+
``git_gd_qt.py`` helper before launching ``git difftool`` for a selected
|
|
111
|
+
file. Run ``pydifft gd --install`` to add the matching ``git gd`` alias
|
|
112
|
+
to your global git config.
|
|
109
113
|
- `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
|
|
110
114
|
the bundled templates and example ``project1`` hierarchy, then downloads
|
|
111
115
|
MathJax into ``_template/mathjax`` so the builder can run immediately.
|
|
@@ -51,14 +51,19 @@ pydifftools/command_line.py
|
|
|
51
51
|
pydifftools/command_registry.py
|
|
52
52
|
pydifftools/comment_functions.py
|
|
53
53
|
pydifftools/comment_tags.lua
|
|
54
|
+
pydifftools/comment_tags_margin.lua
|
|
55
|
+
pydifftools/comment_tags_no_comments.lua
|
|
54
56
|
pydifftools/comment_toggle.js
|
|
55
57
|
pydifftools/comments.css
|
|
56
58
|
pydifftools/continuous.py
|
|
57
59
|
pydifftools/copy_files.py
|
|
58
60
|
pydifftools/diff-doc.js
|
|
59
61
|
pydifftools/doc_contents.py
|
|
62
|
+
pydifftools/git_gd.py
|
|
63
|
+
pydifftools/git_gd_qt.py
|
|
60
64
|
pydifftools/html_comments.py
|
|
61
65
|
pydifftools/html_uncomments.py
|
|
66
|
+
pydifftools/log_example.py
|
|
62
67
|
pydifftools/match_spaces.py
|
|
63
68
|
pydifftools/onewordify.py
|
|
64
69
|
pydifftools/onewordify_undo.py
|
|
@@ -79,6 +84,7 @@ pydifftools/notebook/__init__.py
|
|
|
79
84
|
pydifftools/notebook/fast_build.py
|
|
80
85
|
pydifftools/notebook/tex_to_qmd.py
|
|
81
86
|
tests/test_browser_lifecycle.py
|
|
87
|
+
tests/test_command_line_help.py
|
|
82
88
|
tests/test_continuous_shutdown.py
|
|
83
89
|
tests/test_rrng.py
|
|
84
90
|
tests/test_tex_to_qmd.py
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
def browser_window_is_alive(browser):
|
|
6
|
+
# Keep all browser liveness checks in one place so watch commands share
|
|
7
|
+
# the same shutdown behavior when a user closes the browser window.
|
|
8
|
+
# Do not probe with execute_script here: page navigations can briefly
|
|
9
|
+
# interrupt script execution even while the window is still open.
|
|
10
|
+
if browser is None:
|
|
11
|
+
return False
|
|
12
|
+
try:
|
|
13
|
+
handles = browser.window_handles
|
|
14
|
+
if not handles:
|
|
15
|
+
return False
|
|
16
|
+
return True
|
|
17
|
+
except Exception:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def close_browser_window(browser):
|
|
22
|
+
# Close a Selenium browser session and ignore errors from already-closed
|
|
23
|
+
# windows so cleanup paths stay simple.
|
|
24
|
+
if browser is None:
|
|
25
|
+
return
|
|
26
|
+
try:
|
|
27
|
+
browser.quit()
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def forward_search_in_browser(browser, search_text):
|
|
33
|
+
# Reuse the same browser-side find logic across cpb and qmdb.
|
|
34
|
+
if browser is None or not search_text:
|
|
35
|
+
return False
|
|
36
|
+
found = browser.execute_script(
|
|
37
|
+
"""
|
|
38
|
+
var searchText = arguments[0];
|
|
39
|
+
if (!window.find) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
var didFind = window.find(searchText);
|
|
43
|
+
if (didFind && window.getSelection) {
|
|
44
|
+
var selection = window.getSelection();
|
|
45
|
+
if (selection.rangeCount > 0) {
|
|
46
|
+
var rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
47
|
+
window.scrollBy(0, rect.top - window.innerHeight / 3);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return didFind;
|
|
51
|
+
""",
|
|
52
|
+
search_text,
|
|
53
|
+
)
|
|
54
|
+
if not found:
|
|
55
|
+
print("forward search did not find text:", search_text)
|
|
56
|
+
# Bring the browser window to the foreground in Linux window managers.
|
|
57
|
+
if os.name == "posix" and shutil.which("wmctrl"):
|
|
58
|
+
window_title = browser.execute_script("return document.title;")
|
|
59
|
+
if window_title:
|
|
60
|
+
# Try common Chromium title forms used by desktop environments.
|
|
61
|
+
for title_candidate in [
|
|
62
|
+
window_title,
|
|
63
|
+
window_title + " - Google Chrome",
|
|
64
|
+
window_title + " - Chromium",
|
|
65
|
+
window_title + " - Chrome",
|
|
66
|
+
]:
|
|
67
|
+
subprocess.run(["wmctrl", "-a", title_candidate], check=False)
|
|
68
|
+
return found
|
|
@@ -27,10 +27,16 @@ from .comment_functions import matchingbrackets
|
|
|
27
27
|
from .copy_files import copy_image_files
|
|
28
28
|
from .searchacro import replace_acros
|
|
29
29
|
from .rearrange_tex import run as rearrange_tex_run
|
|
30
|
+
from .git_gd import gd # registers git difftool review command
|
|
30
31
|
from .flowchart.watch_graph import wgrph
|
|
31
32
|
from .flowchart.graph import load_graph_yaml
|
|
32
33
|
from .notebook.tex_to_qmd import tex2qmd
|
|
33
|
-
from .notebook.fast_build import
|
|
34
|
+
from .notebook.fast_build import (
|
|
35
|
+
qmdb,
|
|
36
|
+
qmdinit,
|
|
37
|
+
QMDB_FORWARD_SEARCH_HOST,
|
|
38
|
+
QMDB_FORWARD_SEARCH_PORT,
|
|
39
|
+
)
|
|
34
40
|
|
|
35
41
|
from .command_registry import _COMMAND_SPECS, register_command
|
|
36
42
|
|
|
@@ -518,27 +524,48 @@ def fs(arguments):
|
|
|
518
524
|
"markdown forward search, use with cpb to jump to text in the browser"
|
|
519
525
|
)
|
|
520
526
|
def mfs(text):
|
|
521
|
-
#
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
527
|
+
# Normalize markdown-specific markup so search phrases match rendered text.
|
|
528
|
+
search_text = text
|
|
529
|
+
marker_match = re.search(r"(?i)@[a-z]{2,4}:", search_text)
|
|
530
|
+
if marker_match:
|
|
531
|
+
search_text = search_text[: marker_match.start()]
|
|
532
|
+
search_text = re.sub(r"\[@[^\]]+\]", " ", search_text)
|
|
533
|
+
search_text = search_text.replace("**", " ").replace("*", " ")
|
|
534
|
+
search_text = re.sub(r"\s+", " ", search_text).strip()
|
|
535
|
+
if not search_text:
|
|
536
|
+
search_text = text.strip()
|
|
537
|
+
|
|
538
|
+
# Try existing cpb and qmdb listeners before launching a new cpb process.
|
|
539
|
+
socket_addresses = [
|
|
540
|
+
(FORWARD_SEARCH_HOST, FORWARD_SEARCH_PORT),
|
|
541
|
+
(QMDB_FORWARD_SEARCH_HOST, QMDB_FORWARD_SEARCH_PORT),
|
|
542
|
+
]
|
|
543
|
+
client = None
|
|
544
|
+
for address in socket_addresses:
|
|
545
|
+
candidate = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
546
|
+
try:
|
|
547
|
+
candidate.connect(address)
|
|
548
|
+
client = candidate
|
|
549
|
+
break
|
|
550
|
+
except OSError:
|
|
551
|
+
candidate.close()
|
|
552
|
+
if client is None:
|
|
553
|
+
# If no listener is running yet, choose a markdown file in this
|
|
554
|
+
# directory that contains the requested search text and start cpb for
|
|
555
|
+
# it.
|
|
529
556
|
matching_files = []
|
|
530
557
|
for filename in sorted(os.listdir(".")):
|
|
531
558
|
if not filename.endswith(".md"):
|
|
532
559
|
continue
|
|
533
560
|
with open(filename, encoding="utf-8") as fp:
|
|
534
|
-
if
|
|
561
|
+
if search_text in fp.read():
|
|
535
562
|
matching_files.append(filename)
|
|
536
563
|
if len(matching_files) == 0:
|
|
537
564
|
raise RuntimeError(
|
|
538
|
-
"Could not connect to cpb forward search
|
|
565
|
+
"Could not connect to cpb or qmdb forward search sockets and "
|
|
539
566
|
"could not find the requested text in any .md file in the "
|
|
540
567
|
"current directory."
|
|
541
|
-
)
|
|
568
|
+
)
|
|
542
569
|
if len(matching_files) > 1:
|
|
543
570
|
print(
|
|
544
571
|
"Found search text in multiple markdown files. "
|
|
@@ -554,19 +581,23 @@ def mfs(text):
|
|
|
554
581
|
# Wait up to 20 seconds so the child can bring up the listener,
|
|
555
582
|
# then resend the forward-search text.
|
|
556
583
|
for _ in range(80):
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
584
|
+
for address in socket_addresses:
|
|
585
|
+
candidate = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
586
|
+
try:
|
|
587
|
+
candidate.connect(address)
|
|
588
|
+
client = candidate
|
|
589
|
+
break
|
|
590
|
+
except OSError:
|
|
591
|
+
candidate.close()
|
|
592
|
+
if client is not None:
|
|
560
593
|
break
|
|
561
|
-
|
|
562
|
-
client.close()
|
|
563
|
-
time.sleep(0.25)
|
|
594
|
+
time.sleep(0.25)
|
|
564
595
|
else:
|
|
565
596
|
raise RuntimeError(
|
|
566
597
|
"Started cpb automatically, but the forward search socket "
|
|
567
598
|
"did not come up within 20 seconds."
|
|
568
599
|
)
|
|
569
|
-
client.sendall(
|
|
600
|
+
client.sendall(search_text.encode("utf-8"))
|
|
570
601
|
client.close()
|
|
571
602
|
|
|
572
603
|
|
|
@@ -841,22 +872,11 @@ def build_parser():
|
|
|
841
872
|
flags = argument["flags"]
|
|
842
873
|
kwargs = dict(argument["kwargs"])
|
|
843
874
|
action = subparser.add_argument(*flags, **kwargs)
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
# Provide YAML-only completions for the flowchart watcher.
|
|
850
|
-
action.completer = FilesCompleter(
|
|
851
|
-
allowednames=["*.yaml", "*.yml"]
|
|
852
|
-
)
|
|
853
|
-
if (
|
|
854
|
-
FilesCompleter is not None
|
|
855
|
-
and name == "cpb"
|
|
856
|
-
and action.dest == "filename"
|
|
857
|
-
):
|
|
858
|
-
# Provide Markdown-only completions for continuous pandoc build.
|
|
859
|
-
action.completer = FilesCompleter(allowednames=["*.md"])
|
|
875
|
+
# {{{ attach filename completer
|
|
876
|
+
allowednames = argument.get("completion_allowednames")
|
|
877
|
+
if FilesCompleter is not None and allowednames is not None:
|
|
878
|
+
action.completer = FilesCompleter(allowednames=allowednames)
|
|
879
|
+
# }}}
|
|
860
880
|
if name == "wgrph" and action.dest == "t":
|
|
861
881
|
# Offer case-insensitive completions for incomplete task names.
|
|
862
882
|
action.completer = wgrph_task_completer
|
|
@@ -888,9 +908,23 @@ def main(argv=None):
|
|
|
888
908
|
file=sys.stderr,
|
|
889
909
|
)
|
|
890
910
|
parser = build_parser()
|
|
911
|
+
# {{{ run argcomplete
|
|
891
912
|
if argcomplete is not None:
|
|
892
|
-
#
|
|
893
|
-
|
|
913
|
+
# Only suggest options after the user types '-' so bare
|
|
914
|
+
# `pydifft <tab>` offers subcommands only.
|
|
915
|
+
try:
|
|
916
|
+
argcomplete.autocomplete(
|
|
917
|
+
parser,
|
|
918
|
+
always_complete_options=False,
|
|
919
|
+
exit_method=os._exit,
|
|
920
|
+
output_stream=None,
|
|
921
|
+
)
|
|
922
|
+
except TypeError as exc:
|
|
923
|
+
if "unexpected keyword argument" not in str(exc):
|
|
924
|
+
raise
|
|
925
|
+
# Test stubs may provide a minimal autocomplete(parser) shim only.
|
|
926
|
+
argcomplete.autocomplete(parser)
|
|
927
|
+
# }}}
|
|
894
928
|
if not argv:
|
|
895
929
|
parser.print_help()
|
|
896
930
|
return
|
|
@@ -900,6 +934,15 @@ def main(argv=None):
|
|
|
900
934
|
parser._pydifft_subparsers[subcommand].print_help()
|
|
901
935
|
return
|
|
902
936
|
namespace = parser.parse_args(argv)
|
|
937
|
+
if (
|
|
938
|
+
namespace.command == "cpb"
|
|
939
|
+
and namespace.comments_to_margin
|
|
940
|
+
and namespace.no_comments
|
|
941
|
+
):
|
|
942
|
+
parser._pydifft_subparsers["cpb"].error(
|
|
943
|
+
"argument --no-comments: not allowed with argument "
|
|
944
|
+
"--comments-to-margin"
|
|
945
|
+
)
|
|
903
946
|
handler = namespace._handler
|
|
904
947
|
handler_kwargs = dict(vars(namespace))
|
|
905
948
|
handler_kwargs.pop("_handler", None)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CommandRegistrationError(Exception):
|
|
6
|
+
"""Exception raised when attempting to register a duplicate subcommand."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Registry that stores all subcommands made available to the CLI dispatcher.
|
|
10
|
+
_COMMAND_SPECS = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_command(
|
|
14
|
+
help_text,
|
|
15
|
+
description=None,
|
|
16
|
+
help=None,
|
|
17
|
+
filename_extensions=None,
|
|
18
|
+
):
|
|
19
|
+
"""Register a command handler for the CLI dispatcher."""
|
|
20
|
+
|
|
21
|
+
def decorator(func):
|
|
22
|
+
name = func.__name__.replace("_", "-")
|
|
23
|
+
if name in _COMMAND_SPECS:
|
|
24
|
+
raise CommandRegistrationError(
|
|
25
|
+
f"Command '{name}' already registered"
|
|
26
|
+
)
|
|
27
|
+
signature = inspect.signature(func)
|
|
28
|
+
parameters = [
|
|
29
|
+
parameter
|
|
30
|
+
for parameter in signature.parameters.values()
|
|
31
|
+
if parameter.kind
|
|
32
|
+
not in [
|
|
33
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
34
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
35
|
+
]
|
|
36
|
+
]
|
|
37
|
+
# {{{ normalize filename extensions
|
|
38
|
+
if filename_extensions is None:
|
|
39
|
+
completion_allowednames = {}
|
|
40
|
+
else:
|
|
41
|
+
completion_allowednames = {}
|
|
42
|
+
for argument_name, extensions in filename_extensions.items():
|
|
43
|
+
if isinstance(extensions, str):
|
|
44
|
+
extensions = [extensions]
|
|
45
|
+
completion_allowednames[argument_name] = []
|
|
46
|
+
for extension in extensions:
|
|
47
|
+
if (
|
|
48
|
+
not isinstance(extension, str)
|
|
49
|
+
or len(extension.strip()) == 0
|
|
50
|
+
):
|
|
51
|
+
raise CommandRegistrationError(
|
|
52
|
+
"filename_extensions must contain non-empty"
|
|
53
|
+
" strings"
|
|
54
|
+
)
|
|
55
|
+
extension = extension.strip()
|
|
56
|
+
if extension.startswith("*."):
|
|
57
|
+
completion_allowednames[argument_name].append(
|
|
58
|
+
extension
|
|
59
|
+
)
|
|
60
|
+
elif extension.startswith("."):
|
|
61
|
+
completion_allowednames[argument_name].append(
|
|
62
|
+
"*" + extension
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
completion_allowednames[argument_name].append(
|
|
66
|
+
"*." + extension
|
|
67
|
+
)
|
|
68
|
+
# }}}
|
|
69
|
+
unknown_arguments = sorted(
|
|
70
|
+
set(completion_allowednames)
|
|
71
|
+
- {parameter.name for parameter in parameters}
|
|
72
|
+
)
|
|
73
|
+
if unknown_arguments:
|
|
74
|
+
raise CommandRegistrationError(
|
|
75
|
+
"filename_extensions references unknown arguments: "
|
|
76
|
+
+ ", ".join(unknown_arguments)
|
|
77
|
+
)
|
|
78
|
+
_COMMAND_SPECS[name] = {
|
|
79
|
+
"handler": func,
|
|
80
|
+
"help": help_text.strip(),
|
|
81
|
+
"description": (
|
|
82
|
+
description if description is not None else help_text
|
|
83
|
+
).strip(),
|
|
84
|
+
"arguments": [],
|
|
85
|
+
}
|
|
86
|
+
argument_help = help if help is not None else {}
|
|
87
|
+
for parameter in parameters:
|
|
88
|
+
flags = []
|
|
89
|
+
kwargs = {}
|
|
90
|
+
if parameter.default is inspect._empty:
|
|
91
|
+
flags.append(parameter.name)
|
|
92
|
+
if parameter.name == "arguments":
|
|
93
|
+
# Most commands accept a raw list of trailing arguments.
|
|
94
|
+
kwargs["nargs"] = argparse.REMAINDER
|
|
95
|
+
kwargs["help"] = argparse.SUPPRESS
|
|
96
|
+
else:
|
|
97
|
+
# Single-letter keywords use a short flag; everything else uses
|
|
98
|
+
# the long two-dash style expected by the CLI.
|
|
99
|
+
dash_prefix = "-" if len(parameter.name) == 1 else "--"
|
|
100
|
+
flags.append(dash_prefix + parameter.name.replace("_", "-"))
|
|
101
|
+
kwargs["default"] = parameter.default
|
|
102
|
+
if isinstance(parameter.default, bool):
|
|
103
|
+
# Boolean flags toggle on or off without needing a value.
|
|
104
|
+
kwargs["action"] = (
|
|
105
|
+
"store_false" if parameter.default else "store_true"
|
|
106
|
+
)
|
|
107
|
+
elif parameter.default is not None:
|
|
108
|
+
kwargs["type"] = type(parameter.default)
|
|
109
|
+
if parameter.name in argument_help:
|
|
110
|
+
kwargs["help"] = argument_help[parameter.name].strip()
|
|
111
|
+
argument_spec = {"flags": flags, "kwargs": kwargs}
|
|
112
|
+
if parameter.name in completion_allowednames:
|
|
113
|
+
argument_spec["completion_allowednames"] = (
|
|
114
|
+
completion_allowednames[parameter.name]
|
|
115
|
+
)
|
|
116
|
+
_COMMAND_SPECS[name]["arguments"].append(argument_spec)
|
|
117
|
+
return func
|
|
118
|
+
|
|
119
|
+
return decorator
|