pyDiffTools 0.1.30__tar.gz → 0.1.31__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.30/pyDiffTools.egg-info → pydifftools-0.1.31}/PKG-INFO +10 -1
- {pydifftools-0.1.30 → pydifftools-0.1.31}/README.rst +9 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31/pyDiffTools.egg-info}/PKG-INFO +10 -1
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pyDiffTools.egg-info/SOURCES.txt +1 -0
- pydifftools-0.1.31/pydifftools/flowchart/comparison.py +331 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/flowchart/graph.py +15 -18
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/flowchart/watch_graph.py +195 -229
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/git_gd_history.py +34 -16
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pyproject.toml +1 -1
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_git_gd_history.py +2 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/LICENSE.md +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/MANIFEST.in +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/continuous.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/wrap_sentences.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/setup.cfg +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_wrap_check.py +0 -0
- {pydifftools-0.1.30 → pydifftools-0.1.31}/tests/test_wrapping.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.31
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -101,6 +101,15 @@ included are (listed in order of fun/utility):
|
|
|
101
101
|
refreshed as you edit the file. This wraps the former
|
|
102
102
|
``flowchart/watch_graph.py`` script so all of its functionality is now
|
|
103
103
|
available through the main ``pydifft`` entry point.
|
|
104
|
+
Add ``--diff-base HEAD`` (or a branch, tag, commit, or quoted reflog
|
|
105
|
+
reference) to compare against that Git snapshot throughout the preview
|
|
106
|
+
session. Additions are green, deletions are red and struck through, and
|
|
107
|
+
unchanged content is muted. Renames share one box; date, status, and
|
|
108
|
+
dependency changes are annotated. Navigation, filters, and reloads retain
|
|
109
|
+
the baseline, whose reference and commit appear below the graph.
|
|
110
|
+
Comparison uses the same repository-relative YAML path; a file absent
|
|
111
|
+
from the revision is treated as an empty plan. Historical file renames
|
|
112
|
+
are not followed, and comparison markup is never written to YAML.
|
|
104
113
|
- `pydifft tex2qmd file.tex` converts LaTeX sources to Quarto markdown.
|
|
105
114
|
The converter preserves custom observation blocks and errata tags while
|
|
106
115
|
translating verbatim/python environments into fenced code blocks so the
|
|
@@ -46,6 +46,15 @@ included are (listed in order of fun/utility):
|
|
|
46
46
|
refreshed as you edit the file. This wraps the former
|
|
47
47
|
``flowchart/watch_graph.py`` script so all of its functionality is now
|
|
48
48
|
available through the main ``pydifft`` entry point.
|
|
49
|
+
Add ``--diff-base HEAD`` (or a branch, tag, commit, or quoted reflog
|
|
50
|
+
reference) to compare against that Git snapshot throughout the preview
|
|
51
|
+
session. Additions are green, deletions are red and struck through, and
|
|
52
|
+
unchanged content is muted. Renames share one box; date, status, and
|
|
53
|
+
dependency changes are annotated. Navigation, filters, and reloads retain
|
|
54
|
+
the baseline, whose reference and commit appear below the graph.
|
|
55
|
+
Comparison uses the same repository-relative YAML path; a file absent
|
|
56
|
+
from the revision is treated as an empty plan. Historical file renames
|
|
57
|
+
are not followed, and comparison markup is never written to YAML.
|
|
49
58
|
- `pydifft tex2qmd file.tex` converts LaTeX sources to Quarto markdown.
|
|
50
59
|
The converter preserves custom observation blocks and errata tags while
|
|
51
60
|
translating verbatim/python environments into fenced code blocks so the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.31
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -101,6 +101,15 @@ included are (listed in order of fun/utility):
|
|
|
101
101
|
refreshed as you edit the file. This wraps the former
|
|
102
102
|
``flowchart/watch_graph.py`` script so all of its functionality is now
|
|
103
103
|
available through the main ``pydifft`` entry point.
|
|
104
|
+
Add ``--diff-base HEAD`` (or a branch, tag, commit, or quoted reflog
|
|
105
|
+
reference) to compare against that Git snapshot throughout the preview
|
|
106
|
+
session. Additions are green, deletions are red and struck through, and
|
|
107
|
+
unchanged content is muted. Renames share one box; date, status, and
|
|
108
|
+
dependency changes are annotated. Navigation, filters, and reloads retain
|
|
109
|
+
the baseline, whose reference and commit appear below the graph.
|
|
110
|
+
Comparison uses the same repository-relative YAML path; a file absent
|
|
111
|
+
from the revision is treated as an empty plan. Historical file renames
|
|
112
|
+
are not followed, and comparison markup is never written to YAML.
|
|
104
113
|
- `pydifft tex2qmd file.tex` converts LaTeX sources to Quarto markdown.
|
|
105
114
|
The converter preserves custom observation blocks and errata tags while
|
|
106
115
|
translating verbatim/python environments into fenced code blocks so the
|
|
@@ -80,6 +80,7 @@ pydifftools/update_check.py
|
|
|
80
80
|
pydifftools/wrap_sentences.py
|
|
81
81
|
pydifftools/xml2xlsx.vbs
|
|
82
82
|
pydifftools/flowchart/__init__.py
|
|
83
|
+
pydifftools/flowchart/comparison.py
|
|
83
84
|
pydifftools/flowchart/dot_to_yaml.py
|
|
84
85
|
pydifftools/flowchart/graph.py
|
|
85
86
|
pydifftools/flowchart/watch_graph.py
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""An immutable Git snapshot and a disposable plan comparison render model."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from difflib import SequenceMatcher
|
|
5
|
+
import html
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
from .graph import (
|
|
12
|
+
_filter_nodes_for_dot,
|
|
13
|
+
_format_label,
|
|
14
|
+
_normalize_graph_dates,
|
|
15
|
+
_node_text_with_due,
|
|
16
|
+
load_graph_yaml,
|
|
17
|
+
trace_ancestors,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PlanComparison:
|
|
22
|
+
def __init__(self, path, revision):
|
|
23
|
+
path = Path(path).resolve()
|
|
24
|
+
try:
|
|
25
|
+
root = Path(
|
|
26
|
+
subprocess.check_output(
|
|
27
|
+
[
|
|
28
|
+
"git",
|
|
29
|
+
"-C",
|
|
30
|
+
str(path.parent),
|
|
31
|
+
"rev-parse",
|
|
32
|
+
"--show-toplevel",
|
|
33
|
+
],
|
|
34
|
+
text=True,
|
|
35
|
+
stderr=subprocess.PIPE,
|
|
36
|
+
).strip()
|
|
37
|
+
).resolve()
|
|
38
|
+
relative = path.relative_to(root).as_posix()
|
|
39
|
+
self.commit = subprocess.check_output(
|
|
40
|
+
[
|
|
41
|
+
"git",
|
|
42
|
+
"-C",
|
|
43
|
+
str(root),
|
|
44
|
+
"rev-parse",
|
|
45
|
+
"--verify",
|
|
46
|
+
"--end-of-options",
|
|
47
|
+
f"{revision}^{{commit}}",
|
|
48
|
+
],
|
|
49
|
+
text=True,
|
|
50
|
+
stderr=subprocess.PIPE,
|
|
51
|
+
).strip()
|
|
52
|
+
listing = subprocess.check_output(
|
|
53
|
+
[
|
|
54
|
+
"git",
|
|
55
|
+
"-C",
|
|
56
|
+
str(root),
|
|
57
|
+
"ls-tree",
|
|
58
|
+
self.commit,
|
|
59
|
+
"--",
|
|
60
|
+
relative,
|
|
61
|
+
],
|
|
62
|
+
stderr=subprocess.PIPE,
|
|
63
|
+
)
|
|
64
|
+
self.reference = revision
|
|
65
|
+
self.data = {"nodes": {}}
|
|
66
|
+
if listing:
|
|
67
|
+
content = subprocess.check_output(
|
|
68
|
+
[
|
|
69
|
+
"git",
|
|
70
|
+
"-C",
|
|
71
|
+
str(root),
|
|
72
|
+
"show",
|
|
73
|
+
f"{self.commit}:{relative}",
|
|
74
|
+
],
|
|
75
|
+
stderr=subprocess.PIPE,
|
|
76
|
+
)
|
|
77
|
+
with tempfile.NamedTemporaryFile(suffix=".yaml") as snapshot:
|
|
78
|
+
snapshot.write(content)
|
|
79
|
+
snapshot.flush()
|
|
80
|
+
self.data = load_graph_yaml(snapshot.name)
|
|
81
|
+
_normalize_graph_dates(self.data)
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
detail = getattr(exc, "stderr", None) or str(exc)
|
|
84
|
+
if isinstance(detail, bytes):
|
|
85
|
+
detail = detail.decode(errors="replace")
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Cannot compare {path} against Git revision {revision!r}: "
|
|
88
|
+
f"{detail}"
|
|
89
|
+
) from exc
|
|
90
|
+
|
|
91
|
+
def correspondence(self, current):
|
|
92
|
+
old = self.data["nodes"]
|
|
93
|
+
new = current["nodes"]
|
|
94
|
+
matches = {name: name for name in old.keys() & new.keys()}
|
|
95
|
+
old_text = {
|
|
96
|
+
k: " ".join(str(v.get("text", "")).split()) for k, v in old.items()
|
|
97
|
+
}
|
|
98
|
+
new_text = {
|
|
99
|
+
k: " ".join(str(v.get("text", "")).split()) for k, v in new.items()
|
|
100
|
+
}
|
|
101
|
+
# {{{ Match unique exact content before conservative edited renames
|
|
102
|
+
remaining_old = set(old) - matches.keys()
|
|
103
|
+
remaining_new = set(new) - set(matches.values())
|
|
104
|
+
for name in sorted(remaining_old):
|
|
105
|
+
candidates = [
|
|
106
|
+
k for k in remaining_new if new_text[k] == old_text[name]
|
|
107
|
+
]
|
|
108
|
+
peers = [k for k in remaining_old if old_text[k] == old_text[name]]
|
|
109
|
+
if len(candidates) == len(peers) == 1:
|
|
110
|
+
matches[name] = candidates[0]
|
|
111
|
+
remaining_old -= matches.keys()
|
|
112
|
+
remaining_new -= set(matches.values())
|
|
113
|
+
scores = {}
|
|
114
|
+
for a in remaining_old:
|
|
115
|
+
for b in remaining_new:
|
|
116
|
+
scores[a, b] = SequenceMatcher(
|
|
117
|
+
None, old_text[a], new_text[b], autojunk=False
|
|
118
|
+
).ratio()
|
|
119
|
+
anchors = dict(matches)
|
|
120
|
+
for (a, b), score in sorted(scores.items()):
|
|
121
|
+
if score < 0.9:
|
|
122
|
+
continue
|
|
123
|
+
alternatives = [
|
|
124
|
+
v
|
|
125
|
+
for (x, y), v in scores.items()
|
|
126
|
+
if (x == a or y == b) and (x, y) != (a, b)
|
|
127
|
+
]
|
|
128
|
+
if alternatives and score - max(alternatives) < 0.1 - 1e-9:
|
|
129
|
+
continue
|
|
130
|
+
agrees = True
|
|
131
|
+
evidence = False
|
|
132
|
+
for relation in ("parents", "children"):
|
|
133
|
+
left = {
|
|
134
|
+
anchors[k]
|
|
135
|
+
for k in old[a].get(relation, [])
|
|
136
|
+
if k in anchors
|
|
137
|
+
}
|
|
138
|
+
right = set(new[b].get(relation, [])) & set(anchors.values())
|
|
139
|
+
evidence |= bool(left or right)
|
|
140
|
+
agrees &= left == right
|
|
141
|
+
if agrees and evidence:
|
|
142
|
+
matches[a] = b
|
|
143
|
+
# }}}
|
|
144
|
+
return matches
|
|
145
|
+
|
|
146
|
+
def label(self, before, after, width):
|
|
147
|
+
"""Diff visible words while retaining the current label's markup."""
|
|
148
|
+
deleting = bool(before) and not after
|
|
149
|
+
if deleting:
|
|
150
|
+
after = before
|
|
151
|
+
old = _format_label(before or "", width)[1:-1]
|
|
152
|
+
new = _format_label(after or "", width)[1:-1]
|
|
153
|
+
old_words = re.findall(r"[^\s<>]+", re.sub(r"<[^>]*>", " ", old))
|
|
154
|
+
pieces = re.findall(r"<[^>]*>|\s+|[^\s<]+", new)
|
|
155
|
+
positions = [
|
|
156
|
+
i
|
|
157
|
+
for i, part in enumerate(pieces)
|
|
158
|
+
if not part.startswith("<") and not part.isspace()
|
|
159
|
+
]
|
|
160
|
+
words = [pieces[i] for i in positions]
|
|
161
|
+
for op, a, b, c, d in SequenceMatcher(
|
|
162
|
+
None, old_words, words, autojunk=False
|
|
163
|
+
).get_opcodes():
|
|
164
|
+
for index in range(c, d):
|
|
165
|
+
color = "#16803c" if op in ("insert", "replace") else "#888888"
|
|
166
|
+
pos = positions[index]
|
|
167
|
+
if deleting:
|
|
168
|
+
pieces[pos] = "<s>" + pieces[pos] + "</s>"
|
|
169
|
+
color = "#c62828"
|
|
170
|
+
pieces[pos] = f'<font color="{color}">{pieces[pos]}</font>'
|
|
171
|
+
if op in ("delete", "replace"):
|
|
172
|
+
removed = " ".join(old_words[a:b])
|
|
173
|
+
removed = f'<font color="#c62828"><s>{removed}</s></font> '
|
|
174
|
+
if c < len(positions):
|
|
175
|
+
pieces[positions[c]] = removed + pieces[positions[c]]
|
|
176
|
+
else:
|
|
177
|
+
pieces.append(removed)
|
|
178
|
+
return "".join(pieces)
|
|
179
|
+
|
|
180
|
+
def render(self, current, width, target=None, completed=False):
|
|
181
|
+
matches = self.correspondence(current)
|
|
182
|
+
old = self.data["nodes"]
|
|
183
|
+
new = current["nodes"]
|
|
184
|
+
reverse = {b: a for a, b in matches.items()}
|
|
185
|
+
if target is not None:
|
|
186
|
+
candidates = set(old) | set(new)
|
|
187
|
+
if target not in candidates:
|
|
188
|
+
candidates = [
|
|
189
|
+
k for k in candidates if k.lower() == target.lower()
|
|
190
|
+
]
|
|
191
|
+
if len(candidates) != 1:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"Task {target!r} not found or ambiguous."
|
|
194
|
+
)
|
|
195
|
+
target = candidates[0]
|
|
196
|
+
visible = []
|
|
197
|
+
views = []
|
|
198
|
+
for graph, focus in (
|
|
199
|
+
(current, matches.get(target, target)),
|
|
200
|
+
(self.data, reverse.get(target, target)),
|
|
201
|
+
):
|
|
202
|
+
if target is not None:
|
|
203
|
+
names = [focus] + trace_ancestors(graph, focus)
|
|
204
|
+
view = _filter_nodes_for_dot(graph, names)
|
|
205
|
+
elif completed:
|
|
206
|
+
view = _filter_nodes_for_dot(graph, graph["nodes"], True)
|
|
207
|
+
else:
|
|
208
|
+
view = graph
|
|
209
|
+
visible.append(set(view["nodes"]))
|
|
210
|
+
views.append(view)
|
|
211
|
+
names = visible[0] | {matches.get(k, k) for k in visible[1]}
|
|
212
|
+
model = {"nodes": {}, "styles": deepcopy(current.get("styles", {}))}
|
|
213
|
+
model["styles"] = {
|
|
214
|
+
**deepcopy(self.data.get("styles", {})),
|
|
215
|
+
**model["styles"],
|
|
216
|
+
}
|
|
217
|
+
self.node_states = {}
|
|
218
|
+
self.edge_states = {}
|
|
219
|
+
for name in sorted(names):
|
|
220
|
+
previous = reverse.get(name, name if name in old else None)
|
|
221
|
+
node = deepcopy(new[name] if name in new else old[name])
|
|
222
|
+
before = old.get(previous, {})
|
|
223
|
+
after = new.get(name, {})
|
|
224
|
+
state = "same"
|
|
225
|
+
if name not in new:
|
|
226
|
+
state = "removed"
|
|
227
|
+
elif previous is None:
|
|
228
|
+
state = "added"
|
|
229
|
+
body = self.label(
|
|
230
|
+
before.get("text", ""), after.get("text", ""), width
|
|
231
|
+
)
|
|
232
|
+
if state in ("added", "removed"):
|
|
233
|
+
prefix = "added:" if state == "added" else "deleted:"
|
|
234
|
+
body = f"<b>{prefix}</b><br/>{body}"
|
|
235
|
+
if previous is not None and previous != name:
|
|
236
|
+
body = (
|
|
237
|
+
f'<font color="#c62828"><s>{html.escape(previous)}</s>'
|
|
238
|
+
"</font><br/>" + body
|
|
239
|
+
)
|
|
240
|
+
state = "changed"
|
|
241
|
+
# {{{ Explicit metadata changes, including date-only view edges
|
|
242
|
+
for key in ("due", "orig_due", "style", "parents", "children"):
|
|
243
|
+
left = before.get(key, "")
|
|
244
|
+
right = after.get(key, "")
|
|
245
|
+
if key in ("parents", "children"):
|
|
246
|
+
equal = {matches.get(k, k) for k in left} == set(right)
|
|
247
|
+
else:
|
|
248
|
+
equal = left == right
|
|
249
|
+
if equal:
|
|
250
|
+
if key == "due" and right:
|
|
251
|
+
dated = dict(after)
|
|
252
|
+
dated.pop("text", None)
|
|
253
|
+
body += (
|
|
254
|
+
"<br/>"
|
|
255
|
+
+ _format_label(_node_text_with_due(dated), width)[
|
|
256
|
+
1:-1
|
|
257
|
+
]
|
|
258
|
+
)
|
|
259
|
+
continue
|
|
260
|
+
body += "<br/>" + self.label(
|
|
261
|
+
html.escape(f"{key}: {left or '(none)'}"),
|
|
262
|
+
html.escape(f"{key}: {right or '(none)'}"),
|
|
263
|
+
width,
|
|
264
|
+
)
|
|
265
|
+
# }}}
|
|
266
|
+
if state == "same" and body.count("#16803c") + body.count(
|
|
267
|
+
"#c62828"
|
|
268
|
+
):
|
|
269
|
+
state = "changed"
|
|
270
|
+
node["_comparison_label"] = body
|
|
271
|
+
node["children"] = []
|
|
272
|
+
node["parents"] = []
|
|
273
|
+
model["nodes"][name] = node
|
|
274
|
+
self.node_states[name] = state
|
|
275
|
+
edges = []
|
|
276
|
+
for graph, mapping in ((self.data, matches), (current, {})):
|
|
277
|
+
edges.append(
|
|
278
|
+
{
|
|
279
|
+
(mapping.get(a, a), mapping.get(b, b))
|
|
280
|
+
for a, node in graph["nodes"].items()
|
|
281
|
+
for b in node.get("children", [])
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
visible_edges = {
|
|
285
|
+
(mapping.get(a, a), mapping.get(b, b))
|
|
286
|
+
for graph, mapping in ((views[1], matches), (views[0], {}))
|
|
287
|
+
for a, node in graph["nodes"].items()
|
|
288
|
+
for b in node.get("children", [])
|
|
289
|
+
}
|
|
290
|
+
for a, b in sorted(visible_edges):
|
|
291
|
+
if a not in names or b not in names:
|
|
292
|
+
continue
|
|
293
|
+
model["nodes"][a]["children"].append(b)
|
|
294
|
+
model["nodes"][b]["parents"].append(a)
|
|
295
|
+
self.edge_states[f"{a}->{b}"] = (
|
|
296
|
+
"same"
|
|
297
|
+
if (a, b) in edges[0] & edges[1]
|
|
298
|
+
else "added" if (a, b) in edges[1] else "removed"
|
|
299
|
+
)
|
|
300
|
+
return model
|
|
301
|
+
|
|
302
|
+
def style_svg(self, root, namespace):
|
|
303
|
+
for group in root.iter(f"{namespace}g"):
|
|
304
|
+
kind = group.get("class")
|
|
305
|
+
if kind not in ("node", "edge"):
|
|
306
|
+
continue
|
|
307
|
+
title = group.find(f"{namespace}title")
|
|
308
|
+
states = self.node_states if kind == "node" else self.edge_states
|
|
309
|
+
state = states.get(title.text if title is not None else "", "same")
|
|
310
|
+
color = {"added": "#16803c", "removed": "#c62828"}.get(
|
|
311
|
+
state, "#888888"
|
|
312
|
+
)
|
|
313
|
+
for item in group.iter():
|
|
314
|
+
tag = item.tag.split("}")[-1]
|
|
315
|
+
if tag in ("polygon", "path", "ellipse", "rect"):
|
|
316
|
+
item.set("stroke", color)
|
|
317
|
+
if kind == "node" and item.get("fill") != "none":
|
|
318
|
+
item.set("fill", "#f5f5f5")
|
|
319
|
+
if kind == "edge" and tag == "polygon":
|
|
320
|
+
item.set("fill", color)
|
|
321
|
+
if state == "removed":
|
|
322
|
+
item.set("stroke-dasharray", "5,3")
|
|
323
|
+
elif tag == "text":
|
|
324
|
+
if state in ("added", "removed"):
|
|
325
|
+
item.set("fill", color)
|
|
326
|
+
elif item.get("fill") not in ("#16803c", "#c62828"):
|
|
327
|
+
item.set("fill", "#888888")
|
|
328
|
+
if state == "removed":
|
|
329
|
+
item.set("text-decoration", "line-through")
|
|
330
|
+
elif state == "changed" and item.text in self.node_states:
|
|
331
|
+
item.set("fill", "#16803c")
|
|
@@ -42,15 +42,11 @@ def _str_presenter(dumper, data: str):
|
|
|
42
42
|
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
dumper.add_representer(str, _str_presenter)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
_register_block_str_presenter()
|
|
45
|
+
# {{{ Register multiline strings with each YAML dumper
|
|
46
|
+
for dumper in (yaml.Dumper, yaml.SafeDumper, IndentDumper):
|
|
47
|
+
if getattr(dumper, "yaml_representers", None) is not None:
|
|
48
|
+
dumper.add_representer(str, _str_presenter)
|
|
49
|
+
# }}}
|
|
54
50
|
|
|
55
51
|
|
|
56
52
|
class EmptyGraphYamlError(ValueError):
|
|
@@ -428,12 +424,6 @@ def endpoint_projects(data):
|
|
|
428
424
|
return projects
|
|
429
425
|
|
|
430
426
|
|
|
431
|
-
def _node_label(text, wrap_width=55):
|
|
432
|
-
if text is None:
|
|
433
|
-
return ""
|
|
434
|
-
return _format_label(text, wrap_width)
|
|
435
|
-
|
|
436
|
-
|
|
437
427
|
def _normalize_graph_dates(data):
|
|
438
428
|
# Normalize due dates to mm/dd/yy so the YAML is consistent across years.
|
|
439
429
|
if "nodes" not in data:
|
|
@@ -482,9 +472,11 @@ def _append_node(
|
|
|
482
472
|
if parent_due is None or not str(parent_due).strip():
|
|
483
473
|
depends_on_undated_parent = True
|
|
484
474
|
break
|
|
485
|
-
label =
|
|
486
|
-
_node_text_with_due(node, depends_on_undated_parent), wrap_width
|
|
475
|
+
label = _format_label(
|
|
476
|
+
_node_text_with_due(node, depends_on_undated_parent) or "", wrap_width
|
|
487
477
|
)
|
|
478
|
+
if "_comparison_label" in node:
|
|
479
|
+
label = "<" + node["_comparison_label"] + ">"
|
|
488
480
|
task_link_line = (
|
|
489
481
|
f'<font point-size="9">__WGRPH_TASK_LINK__:{node_name}</font>'
|
|
490
482
|
)
|
|
@@ -734,6 +726,7 @@ def write_dot_from_yaml(
|
|
|
734
726
|
filter_task=None,
|
|
735
727
|
filter_completed=False,
|
|
736
728
|
resolve_due_date_conflict=None,
|
|
729
|
+
comparison=None,
|
|
737
730
|
):
|
|
738
731
|
data = load_graph_yaml(str(yaml_path), old_data=old_data)
|
|
739
732
|
_normalize_graph_dates(data)
|
|
@@ -847,7 +840,11 @@ def write_dot_from_yaml(
|
|
|
847
840
|
adjust_due_after_parents(name)
|
|
848
841
|
# }}}
|
|
849
842
|
data_for_dot = data
|
|
850
|
-
if
|
|
843
|
+
if comparison is not None:
|
|
844
|
+
data_for_dot = comparison.render(
|
|
845
|
+
data, wrap_width, filter_task, filter_completed
|
|
846
|
+
)
|
|
847
|
+
elif filter_task is not None:
|
|
851
848
|
# Limit the rendered graph to incomplete ancestors of the target task.
|
|
852
849
|
if "nodes" not in data or filter_task not in data["nodes"]:
|
|
853
850
|
matches = [
|