pyDiffTools 0.1.8__tar.gz → 0.1.10__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.
Files changed (81) hide show
  1. {pydifftools-0.1.8/pyDiffTools.egg-info → pydifftools-0.1.10}/PKG-INFO +1 -1
  2. {pydifftools-0.1.8 → pydifftools-0.1.10/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/command_line.py +68 -1
  4. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/flowchart/graph.py +66 -2
  5. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/flowchart/watch_graph.py +13 -3
  6. {pydifftools-0.1.8 → pydifftools-0.1.10}/pyproject.toml +1 -1
  7. {pydifftools-0.1.8 → pydifftools-0.1.10}/LICENSE.md +0 -0
  8. {pydifftools-0.1.8 → pydifftools-0.1.10}/MANIFEST.in +0 -0
  9. {pydifftools-0.1.8 → pydifftools-0.1.10}/README.rst +0 -0
  10. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/_quarto.yml +0 -0
  11. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  12. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  13. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  14. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  15. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  16. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  17. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  18. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  19. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  20. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  21. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  22. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  23. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  24. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  25. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  26. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  27. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  28. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  29. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  30. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  31. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  32. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  33. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/example.qmd +0 -0
  34. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/example.tex +0 -0
  35. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/index.qmd +0 -0
  36. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  37. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  38. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  39. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  40. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/independent.qmd +0 -0
  41. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/index.qmd +0 -0
  42. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  43. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  44. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  45. {pydifftools-0.1.8 → pydifftools-0.1.10}/example_notebook/project1/test_include.qmd +0 -0
  46. {pydifftools-0.1.8 → pydifftools-0.1.10}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  47. {pydifftools-0.1.8 → pydifftools-0.1.10}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  48. {pydifftools-0.1.8 → pydifftools-0.1.10}/pyDiffTools.egg-info/entry_points.txt +0 -0
  49. {pydifftools-0.1.8 → pydifftools-0.1.10}/pyDiffTools.egg-info/requires.txt +0 -0
  50. {pydifftools-0.1.8 → pydifftools-0.1.10}/pyDiffTools.egg-info/top_level.txt +0 -0
  51. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/__init__.py +0 -0
  52. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/check_numbers.py +0 -0
  53. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/command_registry.py +0 -0
  54. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/comment_functions.py +0 -0
  55. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/continuous.py +0 -0
  56. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/copy_files.py +0 -0
  57. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/diff-doc.js +0 -0
  58. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/doc_contents.py +0 -0
  59. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/flowchart/__init__.py +0 -0
  60. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  61. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/html_comments.py +0 -0
  62. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/html_uncomments.py +0 -0
  63. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/match_spaces.py +0 -0
  64. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/notebook/__init__.py +0 -0
  65. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/notebook/fast_build.py +0 -0
  66. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/notebook/tex_to_qmd.py +0 -0
  67. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/onewordify.py +0 -0
  68. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/onewordify_undo.py +0 -0
  69. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/outline.py +0 -0
  70. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/rearrange_tex.py +0 -0
  71. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/searchacro.py +0 -0
  72. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/separate_comments.py +0 -0
  73. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/split_conflict.py +0 -0
  74. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/unseparate_comments.py +0 -0
  75. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/update_check.py +0 -0
  76. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/wrap_sentences.py +0 -0
  77. {pydifftools-0.1.8 → pydifftools-0.1.10}/pydifftools/xml2xlsx.vbs +0 -0
  78. {pydifftools-0.1.8 → pydifftools-0.1.10}/setup.cfg +0 -0
  79. {pydifftools-0.1.8 → pydifftools-0.1.10}/tests/test_rrng.py +0 -0
  80. {pydifftools-0.1.8 → pydifftools-0.1.10}/tests/test_tex_to_qmd.py +0 -0
  81. {pydifftools-0.1.8 → pydifftools-0.1.10}/tests/test_update_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -10,6 +10,7 @@ import re
10
10
  import nbformat
11
11
  import difflib
12
12
  import shutil
13
+ import importlib.util
13
14
  from pathlib import Path
14
15
  from . import (
15
16
  match_spaces,
@@ -26,11 +27,31 @@ from .copy_files import copy_image_files
26
27
  from .searchacro import replace_acros
27
28
  from .rearrange_tex import run as rearrange_tex_run
28
29
  from .flowchart.watch_graph import wgrph
30
+ from .flowchart.graph import load_graph_yaml
29
31
  from .notebook.tex_to_qmd import tex2qmd
30
32
  from .notebook.fast_build import qmdb, qmdinit
31
33
 
32
34
  from .command_registry import _COMMAND_SPECS, register_command
33
35
 
36
+ _ARGCOMPLETE_SPEC = importlib.util.find_spec("argcomplete")
37
+ if (
38
+ _ARGCOMPLETE_SPEC is not None
39
+ and _ARGCOMPLETE_SPEC.submodule_search_locations is not None
40
+ ):
41
+ _ARGCOMPLETE_COMPLETERS_SPEC = importlib.util.find_spec(
42
+ "argcomplete.completers"
43
+ )
44
+ else:
45
+ _ARGCOMPLETE_COMPLETERS_SPEC = None
46
+ if _ARGCOMPLETE_SPEC is not None:
47
+ import argcomplete
48
+ else:
49
+ argcomplete = None
50
+ if _ARGCOMPLETE_COMPLETERS_SPEC is not None:
51
+ from argcomplete.completers import FilesCompleter
52
+ else:
53
+ FilesCompleter = None
54
+
34
55
 
35
56
  def printed_exec(cmd):
36
57
  print("about to execute:\n", cmd)
@@ -57,6 +78,37 @@ def get_data(path):
57
78
  return os.path.join(_ROOT, path)
58
79
 
59
80
 
81
+ def wgrph_task_completer(prefix, parsed_args, **kwargs):
82
+ # Provide case-insensitive task name completions for wgrph -t.
83
+ if parsed_args is None or not hasattr(parsed_args, "yaml"):
84
+ return []
85
+ yaml_path = Path(parsed_args.yaml)
86
+ if not yaml_path.exists():
87
+ return []
88
+ try:
89
+ data = load_graph_yaml(str(yaml_path))
90
+ except Exception:
91
+ return []
92
+ if "nodes" not in data:
93
+ return []
94
+ prefix_lower = prefix.lower()
95
+ matches = []
96
+ for name in data["nodes"]:
97
+ if (
98
+ "style" in data["nodes"][name]
99
+ and data["nodes"][name]["style"] == "completed"
100
+ ):
101
+ continue
102
+ if name.lower().startswith(prefix_lower):
103
+ # Preserve case-insensitive matches even when the typed prefix
104
+ # doesn't match case so argcomplete still accepts the suggestion.
105
+ if name.startswith(prefix):
106
+ matches.append(name)
107
+ else:
108
+ matches.append(prefix + name[len(prefix) :])
109
+ return matches
110
+
111
+
60
112
  def recursive_include_search(directory, basename, does_it_input):
61
113
  with open(
62
114
  os.path.join(directory, basename + ".tex"), "r", encoding="utf-8"
@@ -704,7 +756,19 @@ def build_parser():
704
756
  for argument in arguments:
705
757
  flags = argument["flags"]
706
758
  kwargs = dict(argument["kwargs"])
707
- subparser.add_argument(*flags, **kwargs)
759
+ action = subparser.add_argument(*flags, **kwargs)
760
+ if (
761
+ FilesCompleter is not None
762
+ and name == "wgrph"
763
+ and action.dest == "yaml"
764
+ ):
765
+ # Provide YAML-only completions for the flowchart watcher.
766
+ action.completer = FilesCompleter(
767
+ allowednames=["*.yaml", "*.yml"]
768
+ )
769
+ if name == "wgrph" and action.dest == "t":
770
+ # Offer case-insensitive completions for incomplete task names.
771
+ action.completer = wgrph_task_completer
708
772
  subparser.set_defaults(_handler=spec["handler"])
709
773
  return parser
710
774
 
@@ -732,6 +796,9 @@ def main(argv=None):
732
796
  file=sys.stderr,
733
797
  )
734
798
  parser = build_parser()
799
+ if argcomplete is not None:
800
+ # Enable argcomplete integration when the dependency is available.
801
+ argcomplete.autocomplete(parser)
735
802
  if not argv:
736
803
  parser.print_help()
737
804
  return
@@ -324,7 +324,7 @@ def _node_text_with_due(node):
324
324
  # same visual emphasis used for overdue dates.
325
325
  if is_completed:
326
326
  due_color = "green"
327
- elif due_date > today_date and (due_date - today_date).days <= 7:
327
+ elif (due_date - today_date).days <= 7:
328
328
  due_color = "red"
329
329
  else:
330
330
  due_color = "orange"
@@ -565,6 +565,7 @@ def write_dot_from_yaml(
565
565
  order_by_date=False,
566
566
  old_data=None,
567
567
  validate_due_dates=False,
568
+ filter_task=None,
568
569
  ):
569
570
  data = load_graph_yaml(str(yaml_path), old_data=old_data)
570
571
  _normalize_graph_dates(data)
@@ -611,8 +612,71 @@ def write_dot_from_yaml(
611
612
  parents_to_check.append(
612
613
  (grandparent, path + [grandparent])
613
614
  )
615
+ data_for_dot = data
616
+ if filter_task is not None:
617
+ # Limit the rendered graph to incomplete ancestors of the target task.
618
+ if "nodes" not in data or filter_task not in data["nodes"]:
619
+ # Allow case-insensitive task lookup to align with CLI completion.
620
+ matches = [
621
+ name
622
+ for name in data["nodes"]
623
+ if name.lower() == filter_task.lower()
624
+ ]
625
+ if len(matches) == 1:
626
+ filter_task = matches[0]
627
+ elif len(matches) > 1:
628
+ raise ValueError(
629
+ "Task name is ambiguous when compared case-insensitively: "
630
+ f"'{filter_task}' matches {matches}."
631
+ )
632
+ else:
633
+ raise ValueError(
634
+ f"Task '{filter_task}' not found in flowchart YAML."
635
+ )
636
+ # Include the target task alongside its ancestors in the filtered view.
637
+ ancestors = set([filter_task])
638
+ parents_to_check = list(data["nodes"][filter_task]["parents"])
639
+ while parents_to_check:
640
+ parent = parents_to_check.pop()
641
+ if parent in ancestors:
642
+ continue
643
+ ancestors.add(parent)
644
+ if (
645
+ parent in data["nodes"]
646
+ and "parents" in data["nodes"][parent]
647
+ ):
648
+ for grandparent in data["nodes"][parent]["parents"]:
649
+ parents_to_check.append(grandparent)
650
+ incomplete_ancestors = set()
651
+ for name in ancestors:
652
+ if name not in data["nodes"]:
653
+ continue
654
+ if (
655
+ "style" in data["nodes"][name]
656
+ and data["nodes"][name]["style"] == "completed"
657
+ ):
658
+ continue
659
+ incomplete_ancestors.add(name)
660
+ data_for_dot = {"nodes": {}, "styles": {}}
661
+ if "styles" in data:
662
+ data_for_dot["styles"] = data["styles"]
663
+ for name in incomplete_ancestors:
664
+ data_for_dot["nodes"][name] = dict(data["nodes"][name])
665
+ for name in data_for_dot["nodes"]:
666
+ if "children" in data_for_dot["nodes"][name]:
667
+ data_for_dot["nodes"][name]["children"] = [
668
+ child
669
+ for child in data_for_dot["nodes"][name]["children"]
670
+ if child in incomplete_ancestors
671
+ ]
672
+ if "parents" in data_for_dot["nodes"][name]:
673
+ data_for_dot["nodes"][name]["parents"] = [
674
+ parent
675
+ for parent in data_for_dot["nodes"][name]["parents"]
676
+ if parent in incomplete_ancestors
677
+ ]
614
678
  dot_str = yaml_to_dot(
615
- data, wrap_width=wrap_width, order_by_date=order_by_date
679
+ data_for_dot, wrap_width=wrap_width, order_by_date=order_by_date
616
680
  )
617
681
  Path(dot_path).write_text(dot_str)
618
682
  if update_yaml:
@@ -33,6 +33,7 @@ def build_graph(
33
33
  wrap_width,
34
34
  order_by_date=False,
35
35
  prev_data=None,
36
+ target_task=None,
36
37
  ):
37
38
  # Graphviz is required for dot -> svg rendering.
38
39
  if shutil.which("dot") is None:
@@ -47,6 +48,7 @@ def build_graph(
47
48
  order_by_date=order_by_date,
48
49
  old_data=prev_data,
49
50
  validate_due_dates=True,
51
+ filter_task=target_task,
50
52
  )
51
53
  subprocess.run(
52
54
  ["dot", "-Tsvg", str(dot_file), "-o", str(svg_file)],
@@ -65,6 +67,7 @@ class GraphEventHandler(FileSystemEventHandler):
65
67
  wrap_width,
66
68
  data,
67
69
  order_by_date=False,
70
+ target_task=None,
68
71
  debounce=0.25,
69
72
  ):
70
73
  self.yaml_file = Path(yaml_file)
@@ -74,6 +77,7 @@ class GraphEventHandler(FileSystemEventHandler):
74
77
  self.wrap_width = wrap_width
75
78
  self.data = data
76
79
  self.order_by_date = order_by_date
80
+ self.target_task = target_task
77
81
  self.debounce = debounce
78
82
  self._last_handled = 0.0
79
83
  self._last_mtime = None
@@ -94,6 +98,7 @@ class GraphEventHandler(FileSystemEventHandler):
94
98
  self.wrap_width,
95
99
  self.order_by_date,
96
100
  self.data,
101
+ self.target_task,
97
102
  )
98
103
  _reload_svg(self.driver, self.svg_file)
99
104
  self._last_mtime = self.yaml_file.stat().st_mtime
@@ -106,9 +111,12 @@ class GraphEventHandler(FileSystemEventHandler):
106
111
  "yaml": "Path to the flowchart YAML file",
107
112
  "wrap_width": "Line wrap width used when generating node labels",
108
113
  "d": "Render nodes by date without showing connections",
114
+ "t": (
115
+ "Task name to focus on (show incomplete ancestor tasks only)"
116
+ ),
109
117
  },
110
118
  )
111
- def wgrph(yaml, wrap_width=55, d=False):
119
+ def wgrph(yaml, wrap_width=55, d=False, t=None):
112
120
  # Selenium is only required when actually launching the watcher, so it is
113
121
  # imported here to avoid breaking the command-line tools when the optional
114
122
  # dependency is not installed.
@@ -134,7 +142,9 @@ def wgrph(yaml, wrap_width=55, d=False):
134
142
  html_file = yaml_file.with_suffix(".html")
135
143
 
136
144
  # Use date ordering when requested so boxes appear in calendar order.
137
- data = build_graph(yaml_file, dot_file, svg_file, wrap_width, d)
145
+ # Render the initial graph, optionally restricting to incomplete ancestors
146
+ # of a target task.
147
+ data = build_graph(yaml_file, dot_file, svg_file, wrap_width, d, None, t)
138
148
  html_file.write_text(
139
149
  "<html><body style='margin:0'><embed id='svg-view'"
140
150
  " type='image/svg+xml'"
@@ -144,7 +154,7 @@ def wgrph(yaml, wrap_width=55, d=False):
144
154
  driver = webdriver.Chrome(options=options)
145
155
  driver.get(html_file.resolve().as_uri())
146
156
  event_handler = GraphEventHandler(
147
- yaml_file, dot_file, svg_file, driver, wrap_width, data, d
157
+ yaml_file, dot_file, svg_file, driver, wrap_width, data, d, t
148
158
  )
149
159
  observer = Observer()
150
160
  observer.schedule(event_handler, yaml_file.parent, recursive=False)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.8"
7
+ version = "0.1.10"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
File without changes
File without changes
File without changes
File without changes