pyDiffTools 0.1.27__tar.gz → 0.1.28__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 (93) hide show
  1. {pydifftools-0.1.27/pyDiffTools.egg-info → pydifftools-0.1.28}/PKG-INFO +1 -1
  2. {pydifftools-0.1.27 → pydifftools-0.1.28/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/flowchart/graph.py +133 -36
  4. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/flowchart/watch_graph.py +59 -48
  5. {pydifftools-0.1.27 → pydifftools-0.1.28}/pyproject.toml +1 -1
  6. {pydifftools-0.1.27 → pydifftools-0.1.28}/LICENSE.md +0 -0
  7. {pydifftools-0.1.27 → pydifftools-0.1.28}/MANIFEST.in +0 -0
  8. {pydifftools-0.1.27 → pydifftools-0.1.28}/README.rst +0 -0
  9. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/_quarto.yml +0 -0
  10. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  11. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  12. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  13. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  14. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  15. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  16. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  17. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  18. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  19. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  20. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  21. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  22. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  23. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  24. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  25. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  26. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  27. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  28. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  29. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  30. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  31. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  32. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/example.qmd +0 -0
  33. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/example.tex +0 -0
  34. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/index.qmd +0 -0
  35. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  36. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  37. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  38. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  39. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/independent.qmd +0 -0
  40. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/index.qmd +0 -0
  41. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  42. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  43. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  44. {pydifftools-0.1.27 → pydifftools-0.1.28}/example_notebook/project1/test_include.qmd +0 -0
  45. {pydifftools-0.1.27 → pydifftools-0.1.28}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  46. {pydifftools-0.1.27 → pydifftools-0.1.28}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  47. {pydifftools-0.1.27 → pydifftools-0.1.28}/pyDiffTools.egg-info/entry_points.txt +0 -0
  48. {pydifftools-0.1.27 → pydifftools-0.1.28}/pyDiffTools.egg-info/requires.txt +0 -0
  49. {pydifftools-0.1.27 → pydifftools-0.1.28}/pyDiffTools.egg-info/top_level.txt +0 -0
  50. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/__init__.py +0 -0
  51. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/browser_lifecycle.py +0 -0
  52. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/check_numbers.py +0 -0
  53. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/command_line.py +0 -0
  54. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/command_registry.py +0 -0
  55. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/comment_functions.py +0 -0
  56. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/comment_tags.lua +0 -0
  57. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/comment_tags_margin.lua +0 -0
  58. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/comment_tags_no_comments.lua +0 -0
  59. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/comment_toggle.js +0 -0
  60. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/comments.css +0 -0
  61. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/continuous.py +0 -0
  62. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/copy_files.py +0 -0
  63. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/diff-doc.js +0 -0
  64. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/doc_contents.py +0 -0
  65. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/flowchart/__init__.py +0 -0
  66. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  67. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/git_gd.py +0 -0
  68. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/git_gd_qt.py +0 -0
  69. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/html_comments.py +0 -0
  70. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/html_uncomments.py +0 -0
  71. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/log_example.py +0 -0
  72. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/match_spaces.py +0 -0
  73. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/notebook/__init__.py +0 -0
  74. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/notebook/fast_build.py +0 -0
  75. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/notebook/tex_to_qmd.py +0 -0
  76. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/onewordify.py +0 -0
  77. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/onewordify_undo.py +0 -0
  78. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/outline.py +0 -0
  79. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/rearrange_tex.py +0 -0
  80. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/searchacro.py +0 -0
  81. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/separate_comments.py +0 -0
  82. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/split_conflict.py +0 -0
  83. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/unseparate_comments.py +0 -0
  84. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/update_check.py +0 -0
  85. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/wrap_sentences.py +0 -0
  86. {pydifftools-0.1.27 → pydifftools-0.1.28}/pydifftools/xml2xlsx.vbs +0 -0
  87. {pydifftools-0.1.27 → pydifftools-0.1.28}/setup.cfg +0 -0
  88. {pydifftools-0.1.27 → pydifftools-0.1.28}/tests/test_browser_lifecycle.py +0 -0
  89. {pydifftools-0.1.27 → pydifftools-0.1.28}/tests/test_command_line_help.py +0 -0
  90. {pydifftools-0.1.27 → pydifftools-0.1.28}/tests/test_continuous_shutdown.py +0 -0
  91. {pydifftools-0.1.27 → pydifftools-0.1.28}/tests/test_rrng.py +0 -0
  92. {pydifftools-0.1.27 → pydifftools-0.1.28}/tests/test_tex_to_qmd.py +0 -0
  93. {pydifftools-0.1.27 → pydifftools-0.1.28}/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.27
3
+ Version: 0.1.28
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.27
3
+ Version: 0.1.28
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -321,8 +321,7 @@ def _node_text_with_due(node, depends_on_undated_parent=False):
321
321
 
322
322
  # Completed tasks should always show their calendar date so the original
323
323
  # deadline remains visible even if it was today or overdue when finished.
324
- style_name = str(node.get("style", ""))
325
- is_completed = "complete" in style_name
324
+ is_completed = node_is_completed(node)
326
325
  # Replace the actual date with high-visibility notices when the deadline
327
326
  # is today or overdue. These are rendered in a bold 12 pt font so they are
328
327
  # immediately noticeable in the diagram. Completed tasks skip these
@@ -367,6 +366,68 @@ def _node_text_with_due(node, depends_on_undated_parent=False):
367
366
  return formatted
368
367
 
369
368
 
369
+ def node_is_completed(node):
370
+ return "complete" in str(node.get("style", ""))
371
+
372
+
373
+ def node_is_endpoint(node):
374
+ return "endpoint" in str(node.get("style", ""))
375
+
376
+
377
+ def trace_ancestors(
378
+ data,
379
+ start_name,
380
+ stop_at=None,
381
+ include_stopped=False,
382
+ skip_over=None,
383
+ ):
384
+ ancestors = []
385
+ nodes = data.get("nodes", {})
386
+ if start_name not in nodes:
387
+ return ancestors
388
+ ancestors_to_visit = list(nodes[start_name].get("parents", []))
389
+ already_seen = set([start_name])
390
+ while ancestors_to_visit:
391
+ ancestor = ancestors_to_visit.pop()
392
+ if ancestor in already_seen:
393
+ continue
394
+ already_seen.add(ancestor)
395
+ if ancestor not in nodes:
396
+ continue
397
+ ancestor_node = nodes[ancestor]
398
+ if stop_at is not None and stop_at(ancestor, ancestor_node):
399
+ if include_stopped:
400
+ ancestors.append(ancestor)
401
+ continue
402
+ if skip_over is not None and skip_over(ancestor, ancestor_node):
403
+ for parent in ancestor_node.get("parents", []):
404
+ ancestors_to_visit.append(parent)
405
+ continue
406
+ ancestors.append(ancestor)
407
+ for parent in ancestor_node.get("parents", []):
408
+ ancestors_to_visit.append(parent)
409
+ return ancestors
410
+
411
+
412
+ def endpoint_projects(data):
413
+ endpoints = set()
414
+ for name, node_data in data.get("nodes", {}).items():
415
+ if node_is_endpoint(node_data):
416
+ endpoints.add(name)
417
+
418
+ projects = {}
419
+ for endpoint in sorted(endpoints):
420
+ projects[endpoint] = [endpoint]
421
+ projects[endpoint].extend(
422
+ trace_ancestors(
423
+ data,
424
+ endpoint,
425
+ stop_at=lambda name, _node: name in endpoints,
426
+ )
427
+ )
428
+ return projects
429
+
430
+
370
431
  def _node_label(text, wrap_width=55):
371
432
  if text is None:
372
433
  return ""
@@ -415,6 +476,8 @@ def _append_node(
415
476
  if node_has_due:
416
477
  for parent in node.get("parents", []):
417
478
  parent_node = data["nodes"].get(parent, {})
479
+ if node_is_completed(parent_node):
480
+ continue
418
481
  parent_due = parent_node.get("due")
419
482
  if parent_due is None or not str(parent_due).strip():
420
483
  depends_on_undated_parent = True
@@ -586,6 +649,65 @@ def yaml_to_dot(data, wrap_width=55, order_by_date=False):
586
649
  return "\n".join(lines)
587
650
 
588
651
 
652
+ def _filter_nodes_for_dot(
653
+ data, node_names, include_completed_endpoint_ancestors=False
654
+ ):
655
+ nodes = data.get("nodes", {})
656
+ included = []
657
+ included_set = set()
658
+
659
+ def include_node(name):
660
+ if name not in included_set:
661
+ included.append(name)
662
+ included_set.add(name)
663
+
664
+ for name in node_names:
665
+ if name not in nodes:
666
+ continue
667
+ if node_is_completed(nodes[name]):
668
+ continue
669
+ include_node(name)
670
+
671
+ if include_completed_endpoint_ancestors:
672
+ for name in list(included):
673
+ completed_endpoint_ancestors = trace_ancestors(
674
+ data,
675
+ name,
676
+ stop_at=lambda _name, node: node_is_endpoint(node),
677
+ include_stopped=True,
678
+ skip_over=lambda _name, node: (
679
+ node_is_completed(node) and not node_is_endpoint(node)
680
+ ),
681
+ )
682
+ for ancestor in completed_endpoint_ancestors:
683
+ if ancestor not in nodes:
684
+ continue
685
+ if node_is_completed(nodes[ancestor]) and node_is_endpoint(
686
+ nodes[ancestor]
687
+ ):
688
+ include_node(ancestor)
689
+
690
+ data_for_dot = {"nodes": {}, "styles": {}}
691
+ if "styles" in data:
692
+ data_for_dot["styles"] = data["styles"]
693
+ for name in included:
694
+ data_for_dot["nodes"][name] = dict(nodes[name])
695
+ for name in data_for_dot["nodes"]:
696
+ if "children" in data_for_dot["nodes"][name]:
697
+ data_for_dot["nodes"][name]["children"] = [
698
+ child
699
+ for child in data_for_dot["nodes"][name]["children"]
700
+ if child in included_set
701
+ ]
702
+ if "parents" in data_for_dot["nodes"][name]:
703
+ data_for_dot["nodes"][name]["parents"] = [
704
+ parent
705
+ for parent in data_for_dot["nodes"][name]["parents"]
706
+ if parent in included_set
707
+ ]
708
+ return data_for_dot
709
+
710
+
589
711
  def save_graph_yaml(path, data):
590
712
  # Ensure stored dates are normalized before writing.
591
713
  _normalize_graph_dates(data)
@@ -610,6 +732,7 @@ def write_dot_from_yaml(
610
732
  old_data=None,
611
733
  validate_due_dates=False,
612
734
  filter_task=None,
735
+ filter_completed=False,
613
736
  resolve_due_date_conflict=None,
614
737
  ):
615
738
  data = load_graph_yaml(str(yaml_path), old_data=old_data)
@@ -755,40 +878,14 @@ def write_dot_from_yaml(
755
878
  )
756
879
  # Include the target task alongside its ancestors in the filtered view.
757
880
  ancestors = set([filter_task])
758
- parents_to_check = list(data["nodes"][filter_task]["parents"])
759
- while parents_to_check:
760
- parent = parents_to_check.pop()
761
- if parent in ancestors:
762
- continue
763
- ancestors.add(parent)
764
- if parent in data["nodes"] and "parents" in data["nodes"][parent]:
765
- for grandparent in data["nodes"][parent]["parents"]:
766
- parents_to_check.append(grandparent)
767
- incomplete_ancestors = set()
768
- for name in ancestors:
769
- if name not in data["nodes"]:
770
- continue
771
- if "complete" in str(data["nodes"][name].get("style", "")):
772
- continue
773
- incomplete_ancestors.add(name)
774
- data_for_dot = {"nodes": {}, "styles": {}}
775
- if "styles" in data:
776
- data_for_dot["styles"] = data["styles"]
777
- for name in incomplete_ancestors:
778
- data_for_dot["nodes"][name] = dict(data["nodes"][name])
779
- for name in data_for_dot["nodes"]:
780
- if "children" in data_for_dot["nodes"][name]:
781
- data_for_dot["nodes"][name]["children"] = [
782
- child
783
- for child in data_for_dot["nodes"][name]["children"]
784
- if child in incomplete_ancestors
785
- ]
786
- if "parents" in data_for_dot["nodes"][name]:
787
- data_for_dot["nodes"][name]["parents"] = [
788
- parent
789
- for parent in data_for_dot["nodes"][name]["parents"]
790
- if parent in incomplete_ancestors
791
- ]
881
+ ancestors.update(trace_ancestors(data, filter_task))
882
+ data_for_dot = _filter_nodes_for_dot(data, ancestors)
883
+ elif filter_completed:
884
+ data_for_dot = _filter_nodes_for_dot(
885
+ data,
886
+ data.get("nodes", {}).keys(),
887
+ include_completed_endpoint_ancestors=True,
888
+ )
792
889
  dot_str = yaml_to_dot(
793
890
  data_for_dot, wrap_width=wrap_width, order_by_date=order_by_date
794
891
  )
@@ -26,7 +26,7 @@ from pydifftools.browser_lifecycle import (
26
26
  browser_window_is_alive,
27
27
  close_browser_window,
28
28
  )
29
- from .graph import EmptyGraphYamlError, write_dot_from_yaml
29
+ from .graph import EmptyGraphYamlError, endpoint_projects, write_dot_from_yaml
30
30
 
31
31
 
32
32
  def _reload_svg(driver, svg_src) -> None:
@@ -236,35 +236,48 @@ def _watch_view_state_from_params(params):
236
236
  # reliably switch back to the overview without depending on prior state.
237
237
  order_by_date = False
238
238
  target_task = None
239
+ filter_completed = False
240
+ if "p" in params:
241
+ p_value = params["p"][-1]
242
+ filter_completed = p_value in ("1", "true", "yes", "on", "")
239
243
  if "d" in params:
240
244
  d_value = params["d"][-1]
241
245
  order_by_date = d_value in ("1", "true", "yes", "on", "")
246
+ if order_by_date:
247
+ filter_completed = False
242
248
  if "t" in params:
243
249
  t_value = params["t"][-1]
244
250
  if t_value is not None and str(t_value) != "":
245
251
  target_task = str(t_value)
246
252
  order_by_date = False
247
- return order_by_date, target_task
253
+ filter_completed = False
254
+ return order_by_date, target_task, filter_completed
248
255
 
249
256
 
250
- def _watch_footer_links(order_by_date, target_task=None):
257
+ def _watch_footer_links(
258
+ order_by_date, target_task=None, filter_completed=False
259
+ ):
251
260
  links = []
252
- if order_by_date:
253
- links.append(("/", "project overview"))
254
- else:
261
+ if not order_by_date:
255
262
  links.append(("/?d=1", "date-ordered"))
256
- if target_task is not None and str(target_task).strip():
257
- links.append(("/", "project overview"))
263
+ if not filter_completed:
264
+ links.append(("/?p=1", "full plan"))
265
+ if order_by_date or filter_completed:
266
+ links.append(("/", "project overview"))
267
+ elif target_task is not None and str(target_task).strip():
268
+ links.append(("/", "project overview"))
258
269
  return links
259
270
 
260
271
 
261
- def _watch_html(svg_url, order_by_date, target_task=None):
272
+ def _watch_html(
273
+ svg_url, order_by_date, target_task=None, filter_completed=False
274
+ ):
262
275
  # Keep the SVG as the page's main content so browser zoom behavior matches
263
276
  # the original watcher experience (the graph scales, not just footer text).
264
277
  footer_html = " | ".join(
265
278
  f"<a href='{footer_url}'>{footer_label}</a>"
266
279
  for footer_url, footer_label in _watch_footer_links(
267
- order_by_date, target_task
280
+ order_by_date, target_task, filter_completed
268
281
  )
269
282
  )
270
283
  return (
@@ -406,6 +419,7 @@ def build_graph(
406
419
  order_by_date=False,
407
420
  prev_data=None,
408
421
  target_task=None,
422
+ filter_completed=False,
409
423
  resolve_due_date_conflict=None,
410
424
  ):
411
425
  # Graphviz is required for dot -> svg rendering.
@@ -422,6 +436,7 @@ def build_graph(
422
436
  old_data=prev_data,
423
437
  validate_due_dates=True,
424
438
  filter_task=target_task,
439
+ filter_completed=filter_completed,
425
440
  resolve_due_date_conflict=resolve_due_date_conflict,
426
441
  )
427
442
  subprocess.run(
@@ -437,37 +452,10 @@ def build_graph(
437
452
  _svg_add_task_links(svg_root, namespace)
438
453
 
439
454
  if not order_by_date:
440
- # In dependency view mode, each node explicitly tagged with
441
- # ``style: endpoint`` defines a project color. A project includes the
442
- # endpoint plus ancestors, but stops before any ancestor that is
443
- # itself an endpoint.
444
- endpoints = set()
445
- for name, node_data in data["nodes"].items():
446
- if node_data.get("style") == "endpoint":
447
- endpoints.add(name)
448
-
449
- projects = {}
450
- for endpoint in sorted(endpoints):
451
- projects[endpoint] = [endpoint]
452
- ancestors_to_visit = []
453
- if "parents" in data["nodes"][endpoint]:
454
- for parent in data["nodes"][endpoint]["parents"]:
455
- ancestors_to_visit.append(parent)
456
- already_seen = set([endpoint])
457
- while ancestors_to_visit:
458
- ancestor = ancestors_to_visit.pop()
459
- if ancestor in already_seen:
460
- continue
461
- already_seen.add(ancestor)
462
- if ancestor in endpoints:
463
- continue
464
- projects[endpoint].append(ancestor)
465
- if (
466
- ancestor in data["nodes"]
467
- and "parents" in data["nodes"][ancestor]
468
- ):
469
- for parent in data["nodes"][ancestor]["parents"]:
470
- ancestors_to_visit.append(parent)
455
+ # In dependency view mode, each endpoint style defines a project
456
+ # color. A project includes the endpoint plus ancestors, but stops
457
+ # before any ancestor that is itself an endpoint.
458
+ projects = endpoint_projects(data)
471
459
 
472
460
  title_to_group = {}
473
461
  node_title_to_group = {}
@@ -664,7 +652,11 @@ class GraphEventHandler(FileSystemEventHandler):
664
652
  self.data = data
665
653
  self.resolve_due_date_conflict = resolve_due_date_conflict
666
654
  if state is None:
667
- self.state = {"order_by_date": False, "target_task": None}
655
+ self.state = {
656
+ "order_by_date": False,
657
+ "target_task": None,
658
+ "filter_completed": False,
659
+ }
668
660
  else:
669
661
  self.state = state
670
662
  self.debounce = debounce
@@ -686,6 +678,8 @@ class GraphEventHandler(FileSystemEventHandler):
686
678
  build_kwargs["resolve_due_date_conflict"] = (
687
679
  self.resolve_due_date_conflict
688
680
  )
681
+ if self.state["filter_completed"]:
682
+ build_kwargs["filter_completed"] = True
689
683
  self.data = build_graph(
690
684
  self.yaml_file,
691
685
  self.dot_file,
@@ -775,21 +769,30 @@ class FlowchartPreviewServer:
775
769
  params = urllib.parse.parse_qs(
776
770
  parsed.query, keep_blank_values=True
777
771
  )
778
- order_by_date, target_task = _watch_view_state_from_params(
779
- params
780
- )
772
+ (
773
+ order_by_date,
774
+ target_task,
775
+ filter_completed,
776
+ ) = _watch_view_state_from_params(params)
781
777
 
782
778
  if (
783
779
  order_by_date != event_handler.state["order_by_date"]
784
780
  or target_task != event_handler.state["target_task"]
781
+ or filter_completed
782
+ != event_handler.state["filter_completed"]
785
783
  ):
786
784
  event_handler.state["order_by_date"] = order_by_date
787
785
  event_handler.state["target_task"] = target_task
786
+ event_handler.state["filter_completed"] = (
787
+ filter_completed
788
+ )
788
789
  build_kwargs = {}
789
790
  if event_handler.resolve_due_date_conflict is not None:
790
791
  build_kwargs["resolve_due_date_conflict"] = (
791
792
  event_handler.resolve_due_date_conflict
792
793
  )
794
+ if event_handler.state["filter_completed"]:
795
+ build_kwargs["filter_completed"] = True
793
796
  event_handler.data = build_graph(
794
797
  event_handler.yaml_file,
795
798
  event_handler.dot_file,
@@ -805,6 +808,7 @@ class FlowchartPreviewServer:
805
808
  "/graph.svg",
806
809
  event_handler.state["order_by_date"],
807
810
  event_handler.state["target_task"],
811
+ event_handler.state["filter_completed"],
808
812
  )
809
813
  body_bytes = body.encode("utf-8")
810
814
  _send_preview_response(
@@ -850,10 +854,11 @@ class FlowchartPreviewServer:
850
854
  "wrap_width": "Line wrap width used when generating node labels",
851
855
  "d": "Render nodes by date without showing connections",
852
856
  "t": "Task name to focus on (show incomplete ancestor tasks only)",
857
+ "p": "Render the full plan with completed tasks filtered out",
853
858
  },
854
859
  filename_extensions={"yaml": (".yaml", ".yml")},
855
860
  )
856
- def wgrph(yaml, wrap_width=55, d=False, t=None):
861
+ def wgrph(yaml, wrap_width=55, d=False, t=None, p=False):
857
862
  # Selenium is only required when actually launching the watcher, so it is
858
863
  # imported here to avoid breaking the command-line tools when the optional
859
864
  # dependency is not installed.
@@ -875,7 +880,11 @@ def wgrph(yaml, wrap_width=55, d=False, t=None):
875
880
 
876
881
  # The browser now drives filtering/date-mode by GET query parameters
877
882
  # handled by the local preview server. Keep Python state in sync there.
878
- initial_state = {"order_by_date": False, "target_task": None}
883
+ initial_state = {
884
+ "order_by_date": False,
885
+ "target_task": None,
886
+ "filter_completed": False,
887
+ }
879
888
 
880
889
  # Build the default dependency graph first. Optional -t / -d args are then
881
890
  # applied by requesting server URLs with query parameters.
@@ -887,7 +896,7 @@ def wgrph(yaml, wrap_width=55, d=False, t=None):
887
896
  initial_state["order_by_date"],
888
897
  None,
889
898
  initial_state["target_task"],
890
- _resolve_due_date_conflict_with_qt,
899
+ resolve_due_date_conflict=_resolve_due_date_conflict_with_qt,
891
900
  )
892
901
 
893
902
  options = Options()
@@ -921,6 +930,8 @@ def wgrph(yaml, wrap_width=55, d=False, t=None):
921
930
  )
922
931
  elif d:
923
932
  driver.get(preview_server.base_url + "?d=1")
933
+ elif p:
934
+ driver.get(preview_server.base_url + "?p=1")
924
935
 
925
936
  observer = Observer()
926
937
  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.27"
7
+ version = "0.1.28"
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