interactive-pipe 0.9.0__tar.gz → 0.9.2__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 (128) hide show
  1. {interactive_pipe-0.9.0/src/interactive_pipe.egg-info → interactive_pipe-0.9.2}/PKG-INFO +18 -3
  2. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/pyproject.toml +14 -1
  3. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/readme.md +6 -2
  4. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/__init__.py +7 -1
  5. interactive_pipe-0.9.2/src/interactive_pipe/cli.py +216 -0
  6. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/backend.py +1 -0
  7. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/cache.py +31 -1
  8. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/context.py +37 -6
  9. interactive_pipe-0.9.2/src/interactive_pipe/core/context_tracking.py +318 -0
  10. interactive_pipe-0.9.2/src/interactive_pipe/core/engine.py +411 -0
  11. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/filter.py +9 -2
  12. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/framework_state.py +8 -1
  13. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/graph.py +8 -15
  14. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/pipeline.py +87 -12
  15. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/data_objects/audio.py +2 -0
  16. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/data_objects/curves.py +38 -24
  17. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/data_objects/data.py +3 -4
  18. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/data_objects/image.py +23 -6
  19. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/data_objects/parameters.py +10 -1
  20. interactive_pipe-0.9.2/src/interactive_pipe/graphical/actions.py +56 -0
  21. interactive_pipe-0.9.2/src/interactive_pipe/graphical/dpg_backend.py +77 -0
  22. interactive_pipe-0.9.2/src/interactive_pipe/graphical/dpg_control.py +645 -0
  23. interactive_pipe-0.9.2/src/interactive_pipe/graphical/dpg_gui.py +658 -0
  24. interactive_pipe-0.9.2/src/interactive_pipe/graphical/dpg_image.py +367 -0
  25. interactive_pipe-0.9.2/src/interactive_pipe/graphical/dpg_panels.py +205 -0
  26. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/gradio_control.py +129 -3
  27. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/gradio_gui.py +3 -4
  28. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/gradio_layout.py +28 -36
  29. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/gradio_outputs.py +5 -3
  30. interactive_pipe-0.9.2/src/interactive_pipe/graphical/gui.py +577 -0
  31. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/mpl_control.py +62 -4
  32. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/mpl_gui.py +22 -51
  33. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/mpl_window.py +56 -30
  34. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/nb_control.py +68 -4
  35. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/nb_gui.py +1 -1
  36. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/qt_audio.py +2 -2
  37. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/qt_backend.py +63 -8
  38. interactive_pipe-0.9.2/src/interactive_pipe/graphical/qt_control.py +727 -0
  39. interactive_pipe-0.9.2/src/interactive_pipe/graphical/qt_gui.py +975 -0
  40. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/qt_image.py +34 -9
  41. interactive_pipe-0.9.2/src/interactive_pipe/graphical/qt_runner.py +78 -0
  42. interactive_pipe-0.9.2/src/interactive_pipe/graphical/qt_viewer.py +334 -0
  43. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/qt_widgets.py +75 -1
  44. interactive_pipe-0.9.2/src/interactive_pipe/graphical/window.py +198 -0
  45. interactive_pipe-0.9.2/src/interactive_pipe/headless/animation.py +183 -0
  46. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/headless/control.py +145 -3
  47. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/headless/panel.py +2 -2
  48. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/headless/pipeline.py +46 -90
  49. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/helper/choose_backend.py +30 -8
  50. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/helper/filter_decorator.py +14 -6
  51. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/helper/keyword_args_analyzer.py +5 -0
  52. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/helper/pipeline_decorator.py +49 -9
  53. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2/src/interactive_pipe.egg-info}/PKG-INFO +18 -3
  54. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe.egg-info/SOURCES.txt +34 -1
  55. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe.egg-info/requires.txt +12 -0
  56. interactive_pipe-0.9.2/test/test_animation.py +274 -0
  57. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_cache.py +16 -0
  58. interactive_pipe-0.9.2/test/test_cli.py +329 -0
  59. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_curves.py +44 -0
  60. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_data_objects_exceptions.py +17 -9
  61. interactive_pipe-0.9.2/test/test_decorator_default_override.py +16 -0
  62. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_edge_cases.py +2 -6
  63. interactive_pipe-0.9.2/test/test_event_cache.py +79 -0
  64. interactive_pipe-0.9.2/test/test_graph_cache.py +564 -0
  65. interactive_pipe-0.9.2/test/test_gui_actions.py +237 -0
  66. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_headless_exceptions.py +7 -0
  67. interactive_pipe-0.9.2/test/test_image.py +42 -0
  68. interactive_pipe-0.9.2/test/test_layout_grid.py +59 -0
  69. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_qt_backend_cascade.py +32 -2
  70. interactive_pipe-0.9.2/test/test_qt_refresh.py +447 -0
  71. interactive_pipe-0.9.2/test/test_qt_viewer.py +274 -0
  72. interactive_pipe-0.9.2/test/test_range_control.py +157 -0
  73. interactive_pipe-0.9.2/test/test_range_control_dpg.py +94 -0
  74. interactive_pipe-0.9.2/test/test_range_control_gradio.py +46 -0
  75. interactive_pipe-0.9.2/test/test_range_control_mpl.py +52 -0
  76. interactive_pipe-0.9.2/test/test_range_control_nb.py +45 -0
  77. interactive_pipe-0.9.2/test/test_range_control_qt.py +116 -0
  78. interactive_pipe-0.9.2/test/test_readonly_inputs.py +294 -0
  79. interactive_pipe-0.9.2/test/test_reset_parameters.py +105 -0
  80. interactive_pipe-0.9.2/test/test_run_report.py +106 -0
  81. interactive_pipe-0.9.2/test/test_session.py +232 -0
  82. interactive_pipe-0.9.2/test/test_smoke_dpg.py +959 -0
  83. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_smoke_gradio.py +50 -0
  84. interactive_pipe-0.9.2/test/test_smoke_mpl.py +190 -0
  85. interactive_pipe-0.9.2/test/test_smoke_nb.py +78 -0
  86. interactive_pipe-0.9.2/test/test_smoke_qt.py +478 -0
  87. interactive_pipe-0.9.2/test/test_title_elide.py +107 -0
  88. interactive_pipe-0.9.0/src/interactive_pipe/core/engine.py +0 -186
  89. interactive_pipe-0.9.0/src/interactive_pipe/graphical/gui.py +0 -297
  90. interactive_pipe-0.9.0/src/interactive_pipe/graphical/qt_control.py +0 -462
  91. interactive_pipe-0.9.0/src/interactive_pipe/graphical/qt_gui.py +0 -519
  92. interactive_pipe-0.9.0/src/interactive_pipe/graphical/window.py +0 -111
  93. interactive_pipe-0.9.0/test/test_image.py +0 -21
  94. interactive_pipe-0.9.0/test/test_smoke_mpl.py +0 -59
  95. interactive_pipe-0.9.0/test/test_smoke_qt.py +0 -266
  96. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/LICENSE +0 -0
  97. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/setup.cfg +0 -0
  98. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/__init__.py +0 -0
  99. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/core/signature.py +0 -0
  100. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/data_objects/__init__.py +0 -0
  101. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/data_objects/table.py +0 -0
  102. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/__init__.py +0 -0
  103. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/graphical/qt_panels.py +0 -0
  104. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/headless/__init__.py +0 -0
  105. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/headless/keyboard.py +0 -0
  106. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/helper/__init__.py +0 -0
  107. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/helper/_private.py +0 -0
  108. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/helper/control_abbreviation.py +0 -0
  109. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe/py.typed +0 -0
  110. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe.egg-info/dependency_links.txt +0 -0
  111. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/src/interactive_pipe.egg-info/top_level.txt +0 -0
  112. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_context.py +0 -0
  113. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_control_abbreviations.py +0 -0
  114. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_control_registry.py +0 -0
  115. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_controller.py +0 -0
  116. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_core.py +0 -0
  117. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_core_exceptions.py +0 -0
  118. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_decorator.py +0 -0
  119. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_engine.py +0 -0
  120. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_filter.py +0 -0
  121. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_framework_state.py +0 -0
  122. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_graphical_exceptions.py +0 -0
  123. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_headless.py +0 -0
  124. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_legacy_removal.py +0 -0
  125. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_mutable_defaults.py +0 -0
  126. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_parameters.py +0 -0
  127. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_recorder.py +0 -0
  128. {interactive_pipe-0.9.0 → interactive_pipe-0.9.2}/test/test_table.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interactive_pipe
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Library to create flexible interactive image processing pipelines and automatically add a graphical user interface without knowing anything about GUI coding!
5
5
  Author-email: Balthazar Neveu <balthazarneveu@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/balthazarneveu/interactive_pipe
@@ -30,6 +30,14 @@ Requires-Dist: matplotlib>=3.5.3
30
30
  Requires-Dist: numpy>=1.21.6
31
31
  Requires-Dist: Pillow>=9.0.1
32
32
  Requires-Dist: PyYAML>=5.4.1
33
+ Requires-Dist: PyQt6>=6.5.2
34
+ Requires-Dist: PyQt6_sip>=13.5.2
35
+ Requires-Dist: opencv_python_headless>=4.8.1.78
36
+ Requires-Dist: pytest>=6.2.5
37
+ Requires-Dist: ipywidgets>=7.7.1
38
+ Requires-Dist: pandas
39
+ Requires-Dist: gradio
40
+ Requires-Dist: dearpygui>=1.11
33
41
  Provides-Extra: qt6
34
42
  Requires-Dist: PyQt6>=6.5.2; extra == "qt6"
35
43
  Requires-Dist: PyQt6_sip>=13.5.2; extra == "qt6"
@@ -38,6 +46,8 @@ Requires-Dist: PyQt5>=5.15.9; extra == "qt5"
38
46
  Requires-Dist: PyQt5_sip>=12.12.1; extra == "qt5"
39
47
  Provides-Extra: notebook
40
48
  Requires-Dist: ipywidgets>=7.7.1; extra == "notebook"
49
+ Provides-Extra: dpg
50
+ Requires-Dist: dearpygui>=1.11; extra == "dpg"
41
51
  Provides-Extra: pytest
42
52
  Requires-Dist: opencv_python_headless>=4.8.1.78; extra == "pytest"
43
53
  Requires-Dist: pytest>=6.2.5; extra == "pytest"
@@ -49,6 +59,7 @@ Requires-Dist: pytest>=6.2.5; extra == "full"
49
59
  Requires-Dist: ipywidgets>=7.7.1; extra == "full"
50
60
  Requires-Dist: pandas; extra == "full"
51
61
  Requires-Dist: gradio; extra == "full"
62
+ Requires-Dist: dearpygui>=1.11; extra == "full"
52
63
  Provides-Extra: dev
53
64
  Requires-Dist: ruff>=0.9.0; extra == "dev"
54
65
  Requires-Dist: pyright>=1.1.390; extra == "dev"
@@ -93,20 +104,24 @@ pip install interactive-pipe
93
104
  from interactive_pipe import interactive, interactive_pipeline
94
105
  import numpy as np
95
106
 
107
+
96
108
  @interactive(coeff=(1.0, [0.5, 2.0], "exposure"), bias=(0.0, [-0.2, 0.2]))
97
109
  def exposure(img, coeff=1.0, bias=0.0):
98
110
  return img * coeff + bias
99
111
 
112
+
100
113
  @interactive(blend_coeff=(0.5, [0.0, 1.0]))
101
114
  def blend(img0, img1, blend_coeff=0.5):
102
115
  return (1 - blend_coeff) * img0 + blend_coeff * img1
103
116
 
104
- @interactive_pipeline(gui="qt") # or "mpl", "nb" (Jupyter/Colab), "gradio"
117
+
118
+ @interactive_pipeline(gui="qt") # or "dpg" (Dear PyGui), "mpl", "nb" (Jupyter/Colab), "gradio"
105
119
  def pipe(img):
106
120
  exposed = exposure(img)
107
121
  blended = blend(img, exposed)
108
122
  return exposed, blended
109
123
 
124
+
110
125
  pipe(np.array([0.0, 0.5, 0.8]) * np.ones((256, 512, 3)))
111
126
  ```
112
127
 
@@ -119,7 +134,7 @@ Calling `pipe(...)` opens a window with sliders for every declared parameter.
119
134
  | `gui="qt"` | `gui="mpl"` | `gui="nb"` | `gui="gradio"` |
120
135
  | ![qt](docs/images/qt_backend.jpg) | ![mpl](docs/images/mpl_backend.jpg) | ![nb](docs/images/notebook_backend.jpg) | ![gradio](docs/images/gradio_backend.jpg) |
121
136
 
122
- Plus headless mode (`gui=None`) for batch processing. Full feature matrix in the [backends docs](https://balthazarneveu.github.io/interactive_pipe/getting-started/backends/).
137
+ Plus a GPU-rendered [Dear PyGui](https://github.com/hoffstadt/DearPyGui) window (`gui="dpg"`, `pip install "interactive-pipe[dpg]"`) and headless mode (`gui=None`) for batch processing. Full feature matrix in the [backends docs](https://balthazarneveu.github.io/interactive_pipe/getting-started/backends/).
123
138
 
124
139
  ## Learn more
125
140
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "interactive_pipe"
8
- version = "0.9.0"
8
+ version = "0.9.2"
9
9
  authors = [
10
10
  { name="Balthazar Neveu", email="balthazarneveu@gmail.com" },
11
11
  ]
@@ -14,6 +14,14 @@ dependencies = [
14
14
  "numpy>=1.21.6",
15
15
  "Pillow>=9.0.1",
16
16
  "PyYAML>=5.4.1",
17
+ "PyQt6>=6.5.2",
18
+ "PyQt6_sip>=13.5.2",
19
+ "opencv_python_headless>=4.8.1.78",
20
+ "pytest>=6.2.5",
21
+ "ipywidgets>=7.7.1",
22
+ "pandas",
23
+ "gradio",
24
+ "dearpygui>=1.11",
17
25
  ]
18
26
 
19
27
  description = "Library to create flexible interactive image processing pipelines and automatically add a graphical user interface without knowing anything about GUI coding!"
@@ -59,6 +67,10 @@ notebook=[
59
67
  "ipywidgets>=7.7.1",
60
68
  ]
61
69
 
70
+ dpg=[
71
+ "dearpygui>=1.11",
72
+ ]
73
+
62
74
  pytest=[
63
75
  "opencv_python_headless>=4.8.1.78",
64
76
  "pytest>=6.2.5"
@@ -72,6 +84,7 @@ full=[
72
84
  "ipywidgets>=7.7.1",
73
85
  "pandas",
74
86
  "gradio",
87
+ "dearpygui>=1.11",
75
88
  ]
76
89
 
77
90
  dev=[
@@ -33,20 +33,24 @@ pip install interactive-pipe
33
33
  from interactive_pipe import interactive, interactive_pipeline
34
34
  import numpy as np
35
35
 
36
+
36
37
  @interactive(coeff=(1.0, [0.5, 2.0], "exposure"), bias=(0.0, [-0.2, 0.2]))
37
38
  def exposure(img, coeff=1.0, bias=0.0):
38
39
  return img * coeff + bias
39
40
 
41
+
40
42
  @interactive(blend_coeff=(0.5, [0.0, 1.0]))
41
43
  def blend(img0, img1, blend_coeff=0.5):
42
44
  return (1 - blend_coeff) * img0 + blend_coeff * img1
43
45
 
44
- @interactive_pipeline(gui="qt") # or "mpl", "nb" (Jupyter/Colab), "gradio"
46
+
47
+ @interactive_pipeline(gui="qt") # or "dpg" (Dear PyGui), "mpl", "nb" (Jupyter/Colab), "gradio"
45
48
  def pipe(img):
46
49
  exposed = exposure(img)
47
50
  blended = blend(img, exposed)
48
51
  return exposed, blended
49
52
 
53
+
50
54
  pipe(np.array([0.0, 0.5, 0.8]) * np.ones((256, 512, 3)))
51
55
  ```
52
56
 
@@ -59,7 +63,7 @@ Calling `pipe(...)` opens a window with sliders for every declared parameter.
59
63
  | `gui="qt"` | `gui="mpl"` | `gui="nb"` | `gui="gradio"` |
60
64
  | ![qt](docs/images/qt_backend.jpg) | ![mpl](docs/images/mpl_backend.jpg) | ![nb](docs/images/notebook_backend.jpg) | ![gradio](docs/images/gradio_backend.jpg) |
61
65
 
62
- Plus headless mode (`gui=None`) for batch processing. Full feature matrix in the [backends docs](https://balthazarneveu.github.io/interactive_pipe/getting-started/backends/).
66
+ Plus a GPU-rendered [Dear PyGui](https://github.com/hoffstadt/DearPyGui) window (`gui="dpg"`, `pip install "interactive-pipe[dpg]"`) and headless mode (`gui=None`) for batch processing. Full feature matrix in the [backends docs](https://balthazarneveu.github.io/interactive_pipe/getting-started/backends/).
63
67
 
64
68
  ## Learn more
65
69
 
@@ -1,4 +1,4 @@
1
- __version__ = "0.9.0"
1
+ __version__ = "0.9.2"
2
2
  # Backend enum for type-safe backend specification
3
3
  from interactive_pipe.core.backend import Backend
4
4
 
@@ -23,6 +23,7 @@ from interactive_pipe.data_objects.table import Table
23
23
  from interactive_pipe.headless.control import (
24
24
  CircularControl,
25
25
  Control,
26
+ RangeControl,
26
27
  TextPrompt,
27
28
  TimeControl,
28
29
  )
@@ -33,6 +34,9 @@ from interactive_pipe.headless.panel import Panel
33
34
  from interactive_pipe.helper.filter_decorator import interact, interactive
34
35
  from interactive_pipe.helper.pipeline_decorator import interactive_pipeline
35
36
 
37
+ # Animation export (TimeControl pipelines -> GIF / PNG frames)
38
+ from interactive_pipe.headless.animation import export_animation
39
+
36
40
  # Allowing more straightforward naming convention
37
41
  block = interactive
38
42
  pipeline = interactive_pipeline
@@ -57,6 +61,7 @@ __all__ = [
57
61
  "Table",
58
62
  "Control",
59
63
  "CircularControl",
64
+ "RangeControl",
60
65
  "TextPrompt",
61
66
  "TimeControl",
62
67
  "KeyboardControl",
@@ -66,4 +71,5 @@ __all__ = [
66
71
  "interactive_pipeline",
67
72
  "block",
68
73
  "pipeline",
74
+ "export_animation",
69
75
  ]
@@ -0,0 +1,216 @@
1
+ """Command-line and environment helpers to choose the GUI backend and window size.
2
+
3
+ Lets a script pick its backend / window size without editing code::
4
+
5
+ from interactive_pipe import interactive_pipeline
6
+ from interactive_pipe.cli import parse_gui_args
7
+
8
+ args, gui_kwargs = parse_gui_args()
9
+ demo = interactive_pipeline(**gui_kwargs)(my_pipeline) # gui="auto" unless --gui given
10
+ demo(img)
11
+
12
+ Environment variables (read when ``interactive_pipeline`` decorates a function):
13
+
14
+ - ``INTERACTIVE_PIPE_GUI``: one of auto/qt/dpg/mpl/nb/gradio/headless/none,
15
+ used when the decorator was called with ``gui="auto"`` (the default).
16
+ - ``INTERACTIVE_PIPE_SIZE``: "fullscreen", "maximized" or "WxH" (e.g. "1280x800"),
17
+ used when the decorator was not given a ``size``.
18
+
19
+ Precedence: CLI helper values > explicit decorator arguments (other than "auto") > environment.
20
+
21
+ This module only depends on the standard library and has no side effects on import.
22
+ """
23
+
24
+ import argparse
25
+ import os
26
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
27
+
28
+ GUI_ENV_VAR = "INTERACTIVE_PIPE_GUI"
29
+ SIZE_ENV_VAR = "INTERACTIVE_PIPE_SIZE"
30
+
31
+ GUI_CHOICES: Tuple[str, ...] = ("auto", "qt", "dpg", "mpl", "nb", "gradio", "headless")
32
+ # Accepted by the environment variable on top of GUI_CHOICES (aliases of "headless")
33
+ _HEADLESS_ALIASES = ("headless", "none")
34
+
35
+ SizeType = Union[str, Tuple[int, int]]
36
+
37
+
38
+ def parse_size(value: str) -> SizeType:
39
+ """Parse a window size string.
40
+
41
+ Returns "fullscreen" for "fullscreen"/"full", "maximized" for "maximized"/"max",
42
+ or a ``(width, height)`` tuple for "WxH" (e.g. "1280x800").
43
+
44
+ Raises:
45
+ ValueError: If the value is not a recognized size.
46
+ """
47
+ if not isinstance(value, str):
48
+ raise TypeError(f"size should be a string, got {type(value).__name__}")
49
+ normalized = value.strip().lower()
50
+ if normalized in ("fullscreen", "full"):
51
+ return "fullscreen"
52
+ if normalized in ("maximized", "max"):
53
+ return "maximized"
54
+ parts = normalized.split("x")
55
+ if len(parts) == 2 and all(part.strip().isdigit() for part in parts):
56
+ width, height = (int(part) for part in parts)
57
+ if width > 0 and height > 0:
58
+ return (width, height)
59
+ raise ValueError(f"invalid size {value!r}: expected 'fullscreen', 'maximized' or 'WxH' (e.g. 1280x800)")
60
+
61
+
62
+ def _argparse_size(value: str) -> Tuple[int, int]:
63
+ try:
64
+ size = parse_size(value)
65
+ except ValueError as exc:
66
+ raise argparse.ArgumentTypeError(str(exc)) from exc
67
+ if not isinstance(size, tuple):
68
+ raise argparse.ArgumentTypeError(f"--size expects WxH (e.g. 1280x800), use --{size} instead of {value!r}")
69
+ return size
70
+
71
+
72
+ def gui_from_env() -> Optional[str]:
73
+ """Backend requested by ``INTERACTIVE_PIPE_GUI``, or None when unset/empty.
74
+
75
+ "none" is normalized to "headless".
76
+
77
+ Raises:
78
+ ValueError: If the variable holds an unknown backend name.
79
+ """
80
+ raw = os.environ.get(GUI_ENV_VAR, "").strip().lower()
81
+ if not raw:
82
+ return None
83
+ if raw in _HEADLESS_ALIASES:
84
+ return "headless"
85
+ if raw not in GUI_CHOICES:
86
+ raise ValueError(
87
+ f"{GUI_ENV_VAR}={raw!r} is not a valid backend, expected one of {', '.join(GUI_CHOICES + ('none',))}"
88
+ )
89
+ return raw
90
+
91
+
92
+ def size_from_env() -> Optional[SizeType]:
93
+ """Window size requested by ``INTERACTIVE_PIPE_SIZE``, or None when unset/empty.
94
+
95
+ Raises:
96
+ ValueError: If the variable holds an invalid size.
97
+ """
98
+ raw = os.environ.get(SIZE_ENV_VAR, "").strip()
99
+ if not raw:
100
+ return None
101
+ try:
102
+ return parse_size(raw)
103
+ except ValueError as exc:
104
+ raise ValueError(f"{SIZE_ENV_VAR}: {exc}") from exc
105
+
106
+
107
+ def resolve_gui(gui: Any) -> Any:
108
+ """Apply the ``INTERACTIVE_PIPE_GUI`` override to a ``gui`` decorator argument.
109
+
110
+ Only ``"auto"`` (or ``Backend.AUTO``) is overridden; explicit backends and
111
+ ``None`` (headless) are returned unchanged. ``"headless"`` from the
112
+ environment is returned as None.
113
+ """
114
+ if not (isinstance(gui, str) and gui == "auto"):
115
+ return gui
116
+ env_gui = gui_from_env()
117
+ if env_gui is None:
118
+ return gui
119
+ return None if env_gui == "headless" else env_gui
120
+
121
+
122
+ def add_gui_arguments(
123
+ parser: argparse.ArgumentParser, *, defaults: Optional[Dict[str, Any]] = None
124
+ ) -> argparse._ArgumentGroup:
125
+ """Add ``--gui``, ``--fullscreen``, ``--maximized``, ``--size WxH`` and ``--name`` to a parser.
126
+
127
+ Args:
128
+ parser: Parser to extend.
129
+ defaults: Optional default values, keyed by destination
130
+ (``gui``, ``fullscreen``, ``maximized``, ``size``, ``name``).
131
+ A default set here is always reported by ``gui_kwargs_from_args``.
132
+
133
+ Returns:
134
+ The argument group holding the GUI options.
135
+ """
136
+ if defaults is None:
137
+ defaults = {}
138
+ unknown = set(defaults) - {"gui", "fullscreen", "maximized", "size", "name"}
139
+ if unknown:
140
+ raise ValueError(f"unknown GUI argument defaults: {sorted(unknown)}")
141
+ group = parser.add_argument_group("GUI", "interactive_pipe backend and window options")
142
+ group.add_argument(
143
+ "--gui",
144
+ choices=GUI_CHOICES,
145
+ default=defaults.get("gui"),
146
+ help="GUI backend (default: auto, or $%s)" % GUI_ENV_VAR,
147
+ )
148
+ window = group.add_mutually_exclusive_group()
149
+ window.add_argument(
150
+ "--fullscreen",
151
+ action="store_true",
152
+ default=defaults.get("fullscreen", False),
153
+ help="open the window in fullscreen",
154
+ )
155
+ window.add_argument(
156
+ "--maximized",
157
+ action="store_true",
158
+ default=defaults.get("maximized", False),
159
+ help="open the window maximized",
160
+ )
161
+ default_size = defaults.get("size")
162
+ if isinstance(default_size, str):
163
+ default_size = _argparse_size(default_size)
164
+ window.add_argument(
165
+ "--size",
166
+ type=_argparse_size,
167
+ default=default_size,
168
+ metavar="WxH",
169
+ help="window size in pixels, e.g. 1280x800",
170
+ )
171
+ group.add_argument("--name", default=defaults.get("name"), help="window title")
172
+ return group
173
+
174
+
175
+ def gui_kwargs_from_args(args: argparse.Namespace) -> Dict[str, Any]:
176
+ """Keyword arguments for ``interactive_pipeline(**kwargs)`` from parsed arguments.
177
+
178
+ Only options that were given (or have a default) are included, so an empty
179
+ command line yields ``{}`` and keeps the decorator defaults
180
+ (and the environment overrides). ``--gui headless`` maps to ``gui=None``.
181
+ """
182
+ kwargs: Dict[str, Any] = {}
183
+ gui = getattr(args, "gui", None)
184
+ if gui is not None:
185
+ kwargs["gui"] = None if gui == "headless" else gui
186
+ if getattr(args, "fullscreen", False):
187
+ kwargs["size"] = "fullscreen"
188
+ elif getattr(args, "maximized", False):
189
+ kwargs["size"] = "maximized"
190
+ elif getattr(args, "size", None) is not None:
191
+ kwargs["size"] = tuple(args.size)
192
+ name = getattr(args, "name", None)
193
+ if name is not None:
194
+ kwargs["name"] = name
195
+ return kwargs
196
+
197
+
198
+ def parse_gui_args(
199
+ argv: Optional[Sequence[str]] = None, parser: Optional[argparse.ArgumentParser] = None
200
+ ) -> Tuple[argparse.Namespace, Dict[str, Any]]:
201
+ """Parse the command line and return ``(args, gui_kwargs)``.
202
+
203
+ Args:
204
+ argv: Arguments to parse (defaults to ``sys.argv[1:]``).
205
+ parser: Your own parser to extend with the GUI options; a new one is
206
+ created when omitted.
207
+
208
+ Returns:
209
+ The parsed namespace and the keyword arguments for ``interactive_pipeline``.
210
+ """
211
+ if parser is None:
212
+ parser = argparse.ArgumentParser()
213
+ add_gui_arguments(parser)
214
+ argv_list: Optional[List[str]] = list(argv) if argv is not None else None
215
+ args = parser.parse_args(argv_list)
216
+ return args, gui_kwargs_from_args(args)
@@ -5,6 +5,7 @@ class Backend(str, Enum):
5
5
  """Supported GUI backends for interactive pipelines."""
6
6
 
7
7
  QT = "qt"
8
+ DPG = "dpg"
8
9
  GRADIO = "gradio"
9
10
  MPL = "mpl"
10
11
  NB = "nb"
@@ -2,6 +2,36 @@ import logging
2
2
  from copy import deepcopy
3
3
  from typing import Any, Optional
4
4
 
5
+ import numpy as np
6
+
7
+
8
+ def _values_equal(new_value: Any, old_value: Any) -> bool:
9
+ """Equality that also works for array-like parameter values.
10
+
11
+ Filter parameters are usually scalars, but any keyword argument default
12
+ (e.g. a numpy kernel) ends up in the compared dicts, and ``dict != dict``
13
+ raises on arrays ("truth value of an array is ambiguous").
14
+ """
15
+ if new_value is old_value:
16
+ return True
17
+ if isinstance(new_value, np.ndarray) or isinstance(old_value, np.ndarray):
18
+ return np.array_equal(np.asarray(new_value), np.asarray(old_value))
19
+ if isinstance(new_value, dict) and isinstance(old_value, dict):
20
+ return new_value.keys() == old_value.keys() and all(
21
+ _values_equal(new_value[key], old_value[key]) for key in new_value
22
+ )
23
+ if isinstance(new_value, (list, tuple)) and isinstance(old_value, (list, tuple)):
24
+ return (
25
+ type(new_value) is type(old_value)
26
+ and len(new_value) == len(old_value)
27
+ and all(_values_equal(a, b) for a, b in zip(new_value, old_value))
28
+ )
29
+ try:
30
+ return bool(new_value == old_value)
31
+ except (TypeError, ValueError):
32
+ # incomparable objects: conservatively consider them changed
33
+ return False
34
+
5
35
 
6
36
  class CachedResults:
7
37
  """
@@ -88,7 +118,7 @@ class StateChange:
88
118
  :param new_params: The new parameters to check.
89
119
  :return: True if the parameters have changed or False otherwise.
90
120
  """
91
- if self._stored_params is None or new_params != self._stored_params:
121
+ if self._stored_params is None or not _values_equal(new_params, self._stored_params):
92
122
  self._stored_params = deepcopy(new_params)
93
123
  self._update_needed = True
94
124
  else:
@@ -22,6 +22,10 @@ _framework_state: ContextVar[Optional[FrameworkState]] = ContextVar("_ip_framewo
22
22
 
23
23
  _user_context: ContextVar[Optional[Dict[str, Any]]] = ContextVar("_ip_user_context", default=None)
24
24
 
25
+ # name of the filter currently executing (set by filter.run), used to attribute
26
+ # event reads to filters
27
+ _current_filter: ContextVar[Optional[str]] = ContextVar("_ip_current_filter", default=None)
28
+
25
29
 
26
30
  # ============================================================================
27
31
  # Internal Helper Functions (not exported)
@@ -54,6 +58,11 @@ def _set_user_context(ctx: Optional[Dict[str, Any]]) -> None:
54
58
  _user_context.set(ctx)
55
59
 
56
60
 
61
+ def _set_current_filter(name: Optional[str]) -> None:
62
+ """Internal: Set the name of the running filter (called by filter.run)."""
63
+ _current_filter.set(name)
64
+
65
+
57
66
  # ============================================================================
58
67
  # Layout Proxy - Display Control
59
68
  # ============================================================================
@@ -251,13 +260,22 @@ class _EventsProxy:
251
260
  key binding.
252
261
  """
253
262
 
263
+ @staticmethod
264
+ def _record_read(state: FrameworkState, name: str) -> None:
265
+ """Register the running filter as a reader of the event (cache invalidation)."""
266
+ filter_name = _current_filter.get()
267
+ if filter_name is not None:
268
+ state.event_readers.setdefault(name, set()).add(filter_name)
269
+
254
270
  def __getitem__(self, name: str) -> bool:
255
271
  """Strict access: raises KeyError if the event was never bound.
256
272
 
257
273
  Raises:
258
274
  RuntimeError: If called outside of filter execution context.
259
275
  """
260
- return _get_framework_state().events[name]
276
+ state = _get_framework_state()
277
+ self._record_read(state, name)
278
+ return state.events[name]
261
279
 
262
280
  def get(self, name: str, default: bool = False) -> bool:
263
281
  """Return the event flag, or `default` when the event is not bound.
@@ -265,15 +283,22 @@ class _EventsProxy:
265
283
  Raises:
266
284
  RuntimeError: If called outside of filter execution context.
267
285
  """
268
- return _get_framework_state().events.get(name, default)
286
+ state = _get_framework_state()
287
+ self._record_read(state, name)
288
+ return state.events.get(name, default)
269
289
 
270
290
  def __contains__(self, name: str) -> bool:
271
291
  """Check whether an event name is bound."""
272
- return name in _get_framework_state().events
292
+ state = _get_framework_state()
293
+ self._record_read(state, name)
294
+ return name in state.events
273
295
 
274
296
  def keys(self):
275
- """Names of all bound events."""
276
- return _get_framework_state().events.keys()
297
+ """Names of all bound events (registers the filter as a reader of each)."""
298
+ state = _get_framework_state()
299
+ for name in state.events:
300
+ self._record_read(state, name)
301
+ return state.events.keys()
277
302
 
278
303
  def __repr__(self) -> str:
279
304
  try:
@@ -368,10 +393,16 @@ class _ContextProxy:
368
393
 
369
394
  def __getattr__(self, name: str) -> Any:
370
395
  """Get item from context using attribute-style access."""
396
+ if name.startswith("_"):
397
+ # dunder/private lookups come from Python internals and tooling
398
+ # (copy, pickle, IPython completion...), never from user context keys
399
+ raise AttributeError(name)
371
400
  try:
372
401
  return get_context()[name]
373
402
  except KeyError:
374
- raise AttributeError(f"Context has no attribute '{name}'. Available keys: {list(get_context().keys())}")
403
+ raise AttributeError(
404
+ f"Context has no attribute '{name}'. Available keys: {list(get_context().keys())}"
405
+ ) from None
375
406
 
376
407
  def __setattr__(self, name: str, value: Any) -> None:
377
408
  """Set item in context using attribute-style access."""