syd 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl

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.
@@ -0,0 +1,300 @@
1
+ # Testing Principles for SYD Flask Deployment
2
+
3
+ ## Overview
4
+
5
+ Testing an interactive GUI application like the SYD Flask deployment presents unique challenges. While traditional test suites focus on programmatic API testing, interactive GUIs require a different approach that combines several testing methodologies. This document outlines a comprehensive testing strategy for the SYD Flask deployment.
6
+
7
+ ## Testing Pyramid for Interactive GUIs
8
+
9
+ For the SYD Flask deployment, we recommend implementing a modified testing pyramid with the following layers:
10
+
11
+ 1. **Unit Tests** - Test isolated components and functions
12
+ 2. **Integration Tests** - Test interactions between components
13
+ 3. **API Tests** - Test Flask API endpoints
14
+ 4. **Headless Browser Tests** - Test GUI interactions programmatically
15
+ 5. **Visual Regression Tests** - Ensure UI appearance remains consistent
16
+ 6. **Manual Testing Checklists** - Structured human verification
17
+
18
+ ## 1. Unit Tests
19
+
20
+ ### What to Test
21
+ - Individual Flask routes
22
+ - Component HTML generation functions
23
+ - Parameter update handlers
24
+ - State synchronization logic
25
+ - Plot generation utilities
26
+
27
+ ### Implementation Strategy
28
+ ```python
29
+ import unittest
30
+ from unittest import mock
31
+ from syd.flask_deployment.deployer import FlaskDeployer
32
+ from syd.flask_deployment.components import create_component
33
+
34
+ class TestFlaskDeployerComponents(unittest.TestCase):
35
+ def setUp(self):
36
+ self.mock_viewer = mock.MagicMock()
37
+ self.deployer = FlaskDeployer(self.mock_viewer)
38
+
39
+ def test_create_float_component(self):
40
+ mock_param = mock.MagicMock()
41
+ mock_param.name = "test_param"
42
+ mock_param.value = 5.0
43
+ component = create_component(mock_param)
44
+ self.assertIn("test_param", component.html)
45
+ self.assertIn("5.0", component.html)
46
+ ```
47
+
48
+ ## 2. Integration Tests
49
+
50
+ ### What to Test
51
+ - Parameter creation to HTML rendering pipeline
52
+ - Parameter update → state sync → component update flow
53
+ - Plot generation and serving workflow
54
+
55
+ ### Implementation Strategy
56
+ ```python
57
+ def test_parameter_update_state_sync():
58
+ # Create real viewer with test parameters
59
+ viewer = Viewer()
60
+ viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
61
+
62
+ # Create deployer with this viewer
63
+ deployer = FlaskDeployer(viewer)
64
+
65
+ # Update parameter and check if components reflect the change
66
+ deployer._handle_parameter_update('test_param', 5.0)
67
+
68
+ # Verify component has updated
69
+ component = deployer.parameter_components['test_param']
70
+ self.assertEqual(component.value, 5.0)
71
+ ```
72
+
73
+ ## 3. API Tests
74
+
75
+ ### What to Test
76
+ - All Flask endpoints (`/`, `/update/<name>`, `/state`, `/plot`)
77
+ - Response formats and status codes
78
+ - Error handling
79
+ - Race conditions with concurrent requests
80
+
81
+ ### Implementation Strategy
82
+ ```python
83
+ def test_update_parameter_endpoint():
84
+ viewer = Viewer()
85
+ viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
86
+ deployer = FlaskDeployer(viewer)
87
+ app = deployer.app
88
+
89
+ with app.test_client() as client:
90
+ # Test successful update
91
+ response = client.post('/update/test_param',
92
+ json={'value': 5.0})
93
+ self.assertEqual(response.status_code, 200)
94
+ self.assertEqual(viewer.state['test_param'], 5.0)
95
+
96
+ # Test parameter not found
97
+ response = client.post('/update/nonexistent',
98
+ json={'value': 5.0})
99
+ self.assertEqual(response.status_code, 404)
100
+ ```
101
+
102
+ ## 4. Headless Browser Tests
103
+
104
+ ### What to Test
105
+ - User interactions with the web interface
106
+ - Parameter widget interactions
107
+ - UI updates in response to parameter changes
108
+ - Plot refreshes
109
+
110
+ ### Implementation Strategy
111
+
112
+ Use Selenium or Playwright to automate browser interactions:
113
+
114
+ ```python
115
+ from selenium import webdriver
116
+ from selenium.webdriver.common.by import By
117
+
118
+ def test_slider_interaction():
119
+ # Start the Flask app in a separate thread
120
+ viewer = create_test_viewer()
121
+ deployer = FlaskDeployer(viewer)
122
+ thread = threading.Thread(target=deployer.app.run)
123
+ thread.daemon = True
124
+ thread.start()
125
+
126
+ # Use Selenium to interact with the web interface
127
+ driver = webdriver.Chrome()
128
+ driver.get('http://localhost:5000')
129
+
130
+ # Find a slider and interact with it
131
+ slider = driver.find_element(By.ID, 'param_amplitude')
132
+ slider.send_keys(webdriver.Keys.ARROW_RIGHT * 5) # Increase value
133
+
134
+ # Verify the plot updates
135
+ # This requires some way to detect that the plot has changed
136
+ # Could check the src attribute of the image element changes
137
+ time.sleep(1) # Allow time for update
138
+ plot_src = driver.find_element(By.ID, 'plot').get_attribute('src')
139
+ self.assertTrue(len(plot_src) > 0)
140
+
141
+ driver.quit()
142
+ ```
143
+
144
+ ## 5. Visual Regression Tests
145
+
146
+ ### What to Test
147
+ - UI appearance remains consistent across changes
148
+ - Components render correctly with different values
149
+ - Layout adapts properly to different screen sizes
150
+
151
+ ### Implementation Strategy
152
+
153
+ Use tools like Percy or BackstopJS to capture and compare screenshots:
154
+
155
+ ```python
156
+ def test_visual_appearance():
157
+ # Set up test app with standard parameters
158
+ app = create_standard_test_app()
159
+
160
+ # Use a tool like Percy
161
+ percy_snapshot(driver, 'Main view')
162
+
163
+ # Change to different layout
164
+ driver.get('http://localhost:5000?controls_position=right')
165
+ percy_snapshot(driver, 'Right controls layout')
166
+
167
+ # Test responsive views
168
+ driver.set_window_size(400, 800) # Mobile size
169
+ percy_snapshot(driver, 'Mobile view')
170
+ ```
171
+
172
+ ## 6. Manual Testing Checklists
173
+
174
+ For some aspects of interactive GUI applications, manual testing remains necessary. Create structured checklists for testers:
175
+
176
+ ```markdown
177
+ # Manual Testing Checklist
178
+
179
+ ## Parameter Interactions
180
+ - [ ] Sliders respond smoothly to mouse drag
181
+ - [ ] Number inputs accept keyboard input
182
+ - [ ] Range sliders correctly display and update both values
183
+ - [ ] Toggles switch state when clicked
184
+ - [ ] Dropdowns show all options and select correctly
185
+
186
+ ## Plot Updates
187
+ - [ ] Plot updates immediately after parameter change when continuous=True
188
+ - [ ] Plot updates only after interaction ends when continuous=False
189
+ - [ ] Plot maintains aspect ratio when window is resized
190
+ - [ ] No visual glitches during plot transitions
191
+
192
+ ## Layouts
193
+ - [ ] Test all layouts: left, right, top, bottom
194
+ - [ ] Verify responsive behavior on different screen sizes
195
+ ```
196
+
197
+ ## Automated Testing Framework
198
+
199
+ To bring this all together, we recommend implementing a pytest-based testing framework:
200
+
201
+ ```python
202
+ # conftest.py
203
+ import pytest
204
+ from syd import make_viewer
205
+ import numpy as np
206
+ import matplotlib.pyplot as plt
207
+
208
+ @pytest.fixture
209
+ def standard_test_viewer():
210
+ """Create a standard viewer for testing."""
211
+ viewer = make_viewer()
212
+
213
+ def plot(state):
214
+ fig, ax = plt.subplots()
215
+ x = np.linspace(0, 10, 1000)
216
+ y = state['amplitude'] * np.sin(state['frequency'] * x)
217
+ ax.plot(x, y)
218
+ return fig
219
+
220
+ viewer.set_plot(plot)
221
+ viewer.add_float('amplitude', value=1.0, min_value=0, max_value=2)
222
+ viewer.add_float('frequency', value=1.0, min_value=0.1, max_value=5)
223
+
224
+ return viewer
225
+
226
+ @pytest.fixture
227
+ def flask_app(standard_test_viewer):
228
+ """Create a Flask app for testing."""
229
+ from syd.flask_deployment.deployer import FlaskDeployer
230
+ deployer = FlaskDeployer(standard_test_viewer)
231
+ return deployer.app
232
+ ```
233
+
234
+ ## Continuous Integration Setup
235
+
236
+ Integrate these tests into a CI pipeline:
237
+
238
+ ```yaml
239
+ # .github/workflows/flask-tests.yml
240
+ name: Flask Deployment Tests
241
+
242
+ on:
243
+ push:
244
+ paths:
245
+ - 'syd/flask_deployment/**'
246
+ - 'tests/flask/**'
247
+
248
+ jobs:
249
+ test:
250
+ runs-on: ubuntu-latest
251
+ steps:
252
+ - uses: actions/checkout@v2
253
+ - name: Set up Python
254
+ uses: actions/setup-python@v2
255
+ with:
256
+ python-version: '3.9'
257
+ - name: Install dependencies
258
+ run: |
259
+ python -m pip install --upgrade pip
260
+ pip install -e ".[test]"
261
+ - name: Run unit and integration tests
262
+ run: pytest tests/flask/test_unit_integration.py
263
+ - name: Run API tests
264
+ run: pytest tests/flask/test_api.py
265
+ - name: Set up Chrome WebDriver
266
+ uses: browser-actions/setup-chrome@latest
267
+ - name: Run headless browser tests
268
+ run: pytest tests/flask/test_browser.py
269
+ - name: Upload visual test artifacts
270
+ uses: actions/upload-artifact@v2
271
+ with:
272
+ name: visual-test-results
273
+ path: tests/flask/visual/results
274
+ ```
275
+
276
+ ## Practical Implementation Plan
277
+
278
+ For the SYD Flask deployment, we recommend implementing tests in this order:
279
+
280
+ 1. **Start with Unit Tests** - Focus on individual components and functions
281
+ 2. **Add API Tests** - Ensure all endpoints work correctly
282
+ 3. **Implement Integration Tests** - Test component interactions
283
+ 4. **Add Visual Testing** - Simple screenshot comparisons
284
+ 5. **Create Browser Tests** - For critical UI workflows
285
+
286
+ ## Testing Challenges and Solutions
287
+
288
+ | Challenge | Solution |
289
+ |-----------|----------|
290
+ | Testing real-time updates | Use JavaScript triggers and MutationObserver in browser tests |
291
+ | Asynchronous plot generation | Implement wait utilities with timeouts |
292
+ | Visual inconsistencies across platforms | Use Percy or similar to handle cross-platform rendering |
293
+ | Slow browser tests | Run only critical UI tests in CI, more comprehensive ones nightly |
294
+ | Matplotlib backend differences | Mock or standardize backends during testing |
295
+
296
+ ## Conclusion
297
+
298
+ Testing an interactive GUI like the SYD Flask deployment requires a multi-layered approach. By combining traditional unit and integration tests with specialized GUI testing tools, we can achieve good test coverage while still accommodating the unique challenges of interactive web applications. The most effective strategy is to automate as much as possible while maintaining a structured approach to manual testing for aspects that are difficult to verify programmatically.
299
+
300
+ For the initial implementation of the testing framework, focus on establishing solid unit and API tests, as these will provide the foundation for more complex tests and help catch the most common issues early in the development process.
@@ -0,0 +1 @@
1
+ from .deployer import NotebookDeployer
@@ -1,12 +1,14 @@
1
- from typing import Dict, Any, Optional, cast
1
+ from typing import Dict, Any, Optional
2
2
  from dataclasses import dataclass
3
3
  from contextlib import contextmanager
4
4
  import ipywidgets as widgets
5
5
  from IPython.display import display
6
6
  import matplotlib.pyplot as plt
7
+ import warnings
8
+ from ..parameters import ParameterUpdateWarning
7
9
 
8
- from ..interactive_viewer import InteractiveViewer
9
- from .widgets import BaseParameterWidget, create_parameter_widget
10
+ from ..viewer import Viewer
11
+ from .widgets import BaseWidget, create_widget
10
12
 
11
13
 
12
14
  @contextmanager
@@ -26,7 +28,6 @@ class LayoutConfig:
26
28
  figure_width: float = 8.0
27
29
  figure_height: float = 6.0
28
30
  controls_width_percent: int = 30
29
- continuous_update: bool = False
30
31
 
31
32
  def __post_init__(self):
32
33
  valid_positions = ["left", "top"]
@@ -40,31 +41,32 @@ class LayoutConfig:
40
41
  return self.controls_position == "left"
41
42
 
42
43
 
43
- class NotebookDeployment:
44
+ class NotebookDeployer:
44
45
  """
45
- A deployment system for InteractiveViewer in Jupyter notebooks using ipywidgets.
46
+ A deployment system for Viewer in Jupyter notebooks using ipywidgets.
46
47
  Built around the parameter widget system for clean separation of concerns.
47
48
  """
48
49
 
49
50
  def __init__(
50
51
  self,
51
- viewer: InteractiveViewer,
52
+ viewer: Viewer,
52
53
  layout_config: Optional[LayoutConfig] = None,
53
- continuous_update: bool = False,
54
+ continuous: bool = False,
55
+ suppress_warnings: bool = False,
54
56
  ):
55
- if not isinstance(viewer, InteractiveViewer): # type: ignore
56
- raise TypeError(
57
- f"viewer must be an InteractiveViewer, got {type(viewer).__name__}"
58
- )
57
+ if not isinstance(viewer, Viewer): # type: ignore
58
+ raise TypeError(f"viewer must be an Viewer, got {type(viewer).__name__}")
59
59
 
60
60
  self.viewer = viewer
61
61
  self.config = layout_config or LayoutConfig()
62
- self.continuous_update = continuous_update
62
+ self.continuous = continuous
63
+ self.suppress_warnings = suppress_warnings
63
64
 
64
65
  # Initialize containers
65
- self.parameter_widgets: Dict[str, BaseParameterWidget] = {}
66
+ self.parameter_widgets: Dict[str, BaseWidget] = {}
66
67
  self.layout_widgets = self._create_layout_controls()
67
68
  self.plot_output = widgets.Output()
69
+ self._canvas_widget = None
68
70
 
69
71
  # Store current figure
70
72
  self._current_figure = None
@@ -82,7 +84,7 @@ class NotebookDeployment:
82
84
  min=20,
83
85
  max=80,
84
86
  description="Controls Width %",
85
- continuous_update=True,
87
+ continuous=True,
86
88
  layout=widgets.Layout(width="95%"),
87
89
  style={"description_width": "initial"},
88
90
  )
@@ -95,38 +97,52 @@ class NotebookDeployment:
95
97
  def _create_parameter_widgets(self) -> None:
96
98
  """Create widget instances for all parameters."""
97
99
  for name, param in self.viewer.parameters.items():
98
- widget = create_parameter_widget(
100
+ widget = create_widget(
99
101
  param,
100
- continuous_update=self.continuous_update,
102
+ continuous=self.continuous,
101
103
  )
102
104
 
103
105
  # Store in widget dict
104
106
  self.parameter_widgets[name] = widget
105
107
 
106
- def _handle_parameter_change(self, name: str) -> None:
107
- """Handle changes to parameter widgets."""
108
+ def _handle_widget_engagement(self, name: str) -> None:
109
+ """Handle engagement with an interactive widget."""
108
110
  if self._updating:
111
+ print(
112
+ "Already updating -- there's a circular dependency!"
113
+ "This is probably caused by failing to disable callbacks for a parameter."
114
+ "It's a bug --- tell the developer on github issues please."
115
+ )
109
116
  return
110
117
 
111
118
  try:
112
119
  self._updating = True
113
- widget = self.parameter_widgets[name]
114
120
 
115
- if hasattr(widget, "_is_button") and widget._is_button:
116
- parameter = self.viewer.parameters[name]
117
- parameter.callback(parameter)
118
- else:
119
- self.viewer.set_parameter_value(name, widget.value)
121
+ # Optionally suppress warnings during parameter updates
122
+ with warnings.catch_warnings():
123
+ if self.suppress_warnings:
124
+ warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
120
125
 
121
- # Update any widgets that changed due to dependencies
122
- self._sync_widgets_with_state(exclude=name)
126
+ widget = self.parameter_widgets[name]
123
127
 
124
- # Update the plot
125
- self._update_plot()
128
+ if widget._is_action:
129
+ parameter = self.viewer.parameters[name]
130
+ parameter.callback(self.viewer.state)
131
+ else:
132
+ self.viewer.set_parameter_value(name, widget.value)
133
+
134
+ # Update any widgets that changed due to dependencies
135
+ self._sync_widgets_with_state(exclude=name)
136
+
137
+ # Update the plot
138
+ self._update_plot()
126
139
 
127
140
  finally:
128
141
  self._updating = False
129
142
 
143
+ def _handle_action(self, name: str) -> None:
144
+ """Handle actions for parameter widgets."""
145
+
130
146
  def _sync_widgets_with_state(self, exclude: Optional[str] = None) -> None:
131
147
  """Sync widget values with viewer state."""
132
148
  for name, parameter in self.viewer.parameters.items():
@@ -155,20 +171,25 @@ class NotebookDeployment:
155
171
 
156
172
  def _update_plot(self) -> None:
157
173
  """Update the plot with current state."""
158
- state = self.viewer.get_state()
174
+ state = self.viewer.state
159
175
 
160
176
  with _plot_context():
161
177
  new_fig = self.viewer.plot(state)
162
178
  plt.close(self._current_figure) # Close old figure
163
179
  self._current_figure = new_fig
164
180
 
165
- self._redraw_plot()
181
+ # Clear previous output and display new figure
182
+ self.plot_output.clear_output(wait=True)
183
+ with self.plot_output:
184
+ # Make sure the canvas is created and displayed
185
+ if self._canvas_widget is None:
186
+ self._canvas_widget = self._current_figure.canvas
187
+ display(self._current_figure.canvas)
166
188
 
167
189
  def _redraw_plot(self) -> None:
168
190
  """Clear and redraw the plot in the output widget."""
169
- self.plot_output.clear_output(wait=True)
170
- with self.plot_output:
171
- display(self._current_figure)
191
+ if self._canvas_widget is not None:
192
+ self._canvas_widget.draw()
172
193
 
173
194
  def _create_layout(self) -> widgets.Widget:
174
195
  """Create the main layout combining controls and plot."""
@@ -181,7 +202,7 @@ class NotebookDeployment:
181
202
 
182
203
  # Set up parameter widgets with their observe callbacks
183
204
  for name, widget in self.parameter_widgets.items():
184
- widget.observe(lambda change, n=name: self._handle_parameter_change(n))
205
+ widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
185
206
 
186
207
  # Create parameter controls section
187
208
  param_box = widgets.VBox(
@@ -225,7 +246,7 @@ class NotebookDeployment:
225
246
 
226
247
  def deploy(self) -> None:
227
248
  """Deploy the interactive viewer with proper state management."""
228
- with self.viewer.deploy_app():
249
+ with self.viewer._deploy_app():
229
250
  # Create widgets
230
251
  self._create_parameter_widgets()
231
252