Skip to content

Commit 83d76ec

Browse files
committed
[v0.0.5] 2025-08-27
- Helios native C++ had several bugs that was causing errors in the last version. Merged in patched version of 1.3.46. ## Visualizer - Fixed some issues that could cause the visualizer tests to crash in headless mode.
1 parent 49e0bee commit 83d76ec

6 files changed

Lines changed: 91 additions & 37 deletions

File tree

.github/workflows/docs.yml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,13 @@ jobs:
5656
5757
- name: Sync version to Doxygen config
5858
run: |
59-
# Get version from setuptools-scm and update Doxyfile
60-
export PYHELIOS_DEV_MODE=1
61-
VERSION=$(python -c "from pyhelios._version import __version__; print(__version__)")
59+
# Get version directly from _version.py to avoid importing PyHelios
60+
VERSION=$(python -c "
61+
import sys
62+
sys.path.insert(0, 'pyhelios')
63+
from _version import __version__
64+
print(__version__)
65+
")
6266
echo "Updating Doxygen PROJECT_NUMBER to: $VERSION"
6367
sed -i "s/^PROJECT_NUMBER.*$/PROJECT_NUMBER = $VERSION/" docs/Doxyfile.python
6468

@@ -128,9 +132,13 @@ jobs:
128132
129133
- name: Sync version to Doxygen config (preview)
130134
run: |
131-
# Get version from setuptools-scm and update Doxyfile
132-
export PYHELIOS_DEV_MODE=1
133-
VERSION=$(python -c "from pyhelios._version import __version__; print(__version__)")
135+
# Get version directly from _version.py to avoid importing PyHelios
136+
VERSION=$(python -c "
137+
import sys
138+
sys.path.insert(0, 'pyhelios')
139+
from _version import __version__
140+
print(__version__)
141+
")
134142
echo "Updating Doxygen PROJECT_NUMBER to: $VERSION"
135143
sed -i "s/^PROJECT_NUMBER.*$/PROJECT_NUMBER = $VERSION/" docs/Doxyfile.python
136144

docs/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
# [v0.0.5] 2025-08-27
4+
5+
- Helios native C++ had several bugs that was causing errors in the last version. Merged in patched version of 1.3.46.
6+
7+
## Visualizer
8+
- Fixed some issues that could cause the visualizer tests to crash in headless mode.
9+
310
# [v0.0.4] 2025-08-25
411

512
🚨++ New Plug-in Integrated ++ 🚨

helios-core

Submodule helios-core updated 44 files

native/src/pyhelios_wrapper_visualizer.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,31 @@ extern "C" {
1818
//=============================================================================
1919

2020
Visualizer* createVisualizer(unsigned int width, unsigned int height, bool headless) {
21-
// Enable window decorations by default (true), headless parameter controls window visibility
22-
return new Visualizer(width, height, 4, true, headless); // 4 antialiasing samples, decorations enabled
21+
try {
22+
clearError();
23+
// Enable window decorations by default (true), headless parameter controls window visibility
24+
return new Visualizer(width, height, 4, true, headless); // 4 antialiasing samples, decorations enabled
25+
} catch (const std::exception& e) {
26+
setError(PYHELIOS_ERROR_RUNTIME, std::string("ERROR (createVisualizer): Failed to create visualizer: ") + e.what());
27+
return nullptr;
28+
} catch (...) {
29+
setError(PYHELIOS_ERROR_UNKNOWN, "ERROR (createVisualizer): Unknown error creating visualizer. This may indicate OpenGL/graphics initialization problems in headless environments.");
30+
return nullptr;
31+
}
2332
}
2433

2534
Visualizer* createVisualizerWithAntialiasing(unsigned int width, unsigned int height, unsigned int samples, bool headless) {
26-
// Enable window decorations by default (true), headless parameter controls window visibility
27-
return new Visualizer(width, height, samples, true, headless);
35+
try {
36+
clearError();
37+
// Enable window decorations by default (true), headless parameter controls window visibility
38+
return new Visualizer(width, height, samples, true, headless);
39+
} catch (const std::exception& e) {
40+
setError(PYHELIOS_ERROR_RUNTIME, std::string("ERROR (createVisualizerWithAntialiasing): Failed to create visualizer: ") + e.what());
41+
return nullptr;
42+
} catch (...) {
43+
setError(PYHELIOS_ERROR_UNKNOWN, "ERROR (createVisualizerWithAntialiasing): Unknown error creating visualizer. This may indicate OpenGL/graphics initialization problems in headless environments.");
44+
return nullptr;
45+
}
2846
}
2947

3048
void destroyVisualizer(Visualizer* visualizer) {

pyhelios/wrappers/UVisualizerWrapper.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,26 @@ class UVisualizer(ctypes.Structure):
2323
# Visualizer creation and destruction
2424
helios_lib.createVisualizer.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool]
2525
helios_lib.createVisualizer.restype = ctypes.POINTER(UVisualizer)
26+
27+
# Add errcheck to automatically handle errors and nulls
28+
def _check_visualizer_creation(result, func, args):
29+
if _ERROR_MANAGEMENT_AVAILABLE:
30+
check_helios_error(helios_lib.getLastError, helios_lib.getLastErrorMessage)
31+
if not result:
32+
raise RuntimeError(
33+
"Failed to create Visualizer. This may indicate:\n"
34+
"1. OpenGL/graphics initialization problems in headless environments\n"
35+
"2. Missing graphics drivers or display support\n"
36+
"3. Insufficient system resources for graphics context creation\n"
37+
"4. XQuartz or display server configuration issues on macOS/Linux"
38+
)
39+
return result
40+
41+
helios_lib.createVisualizer.errcheck = _check_visualizer_creation
2642

2743
helios_lib.createVisualizerWithAntialiasing.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool]
2844
helios_lib.createVisualizerWithAntialiasing.restype = ctypes.POINTER(UVisualizer)
45+
helios_lib.createVisualizerWithAntialiasing.errcheck = _check_visualizer_creation
2946

3047
helios_lib.destroyVisualizer.argtypes = [ctypes.POINTER(UVisualizer)]
3148
helios_lib.destroyVisualizer.restype = None
@@ -118,25 +135,21 @@ def create_visualizer(width: int, height: int, headless: bool = False) -> Option
118135
)
119136

120137
try:
138+
# The errcheck callback will handle error checking and null pointer validation
121139
visualizer = helios_lib.createVisualizer(
122140
ctypes.c_uint32(width),
123141
ctypes.c_uint32(height),
124142
ctypes.c_bool(headless)
125143
)
126-
_check_for_helios_error()
127-
128-
if not visualizer:
129-
raise RuntimeError("Failed to create Visualizer")
144+
return visualizer
130145
except OSError as e:
131146
# Handle low-level system errors (e.g., graphics context failures)
132147
raise RuntimeError(
133-
f"Visualizer plugin failed to initialize: {e}. "
148+
f"Visualizer plugin failed to initialize due to system error: {e}. "
134149
"This may indicate missing graphics drivers, OpenGL issues, or "
135150
"incompatible system configuration. Try building with different options "
136151
"or check system requirements."
137152
)
138-
139-
return visualizer
140153

141154
def create_visualizer_with_antialiasing(width: int, height: int, antialiasing_samples: int, headless: bool = False) -> Optional[ctypes.POINTER(UVisualizer)]:
142155
"""
@@ -161,18 +174,23 @@ def create_visualizer_with_antialiasing(width: int, height: int, antialiasing_sa
161174
"Rebuild with visualizer plugin enabled."
162175
)
163176

164-
visualizer = helios_lib.createVisualizerWithAntialiasing(
165-
ctypes.c_uint32(width),
166-
ctypes.c_uint32(height),
167-
ctypes.c_uint32(antialiasing_samples),
168-
ctypes.c_bool(headless)
169-
)
170-
_check_for_helios_error()
171-
172-
if not visualizer:
173-
raise RuntimeError("Failed to create Visualizer with antialiasing")
174-
175-
return visualizer
177+
try:
178+
# The errcheck callback will handle error checking and null pointer validation
179+
visualizer = helios_lib.createVisualizerWithAntialiasing(
180+
ctypes.c_uint32(width),
181+
ctypes.c_uint32(height),
182+
ctypes.c_uint32(antialiasing_samples),
183+
ctypes.c_bool(headless)
184+
)
185+
return visualizer
186+
except OSError as e:
187+
# Handle low-level system errors (e.g., graphics context failures)
188+
raise RuntimeError(
189+
f"Visualizer plugin failed to initialize due to system error: {e}. "
190+
"This may indicate missing graphics drivers, OpenGL issues, or "
191+
"incompatible system configuration. Try building with different options "
192+
"or check system requirements."
193+
)
176194

177195
def destroy_visualizer(visualizer: ctypes.POINTER(UVisualizer)) -> None:
178196
"""

tests/test_visualizer.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,11 @@ def test_visualizer_context_manager_protocol(self):
394394

395395
# Clean up
396396
visualizer.__exit__(None, None, None)
397-
except VisualizerError:
398-
# Expected in mock mode
397+
except (VisualizerError, RuntimeError) as e:
398+
# Expected in mock mode or when graphics initialization fails in CI
399+
# Log the specific error for debugging
400+
import sys
401+
print(f"Visualizer initialization failed as expected in CI/mock mode: {e}", file=sys.stderr)
399402
pass
400403

401404
def test_visualizer_method_signatures(self):
@@ -487,8 +490,8 @@ def visualizer_and_context(self):
487490
visualizer = Visualizer(400, 300, headless=True)
488491
visualizer.buildContextGeometry(context)
489492
yield visualizer, context, [patch1, patch2, patch3]
490-
except VisualizerError as e:
491-
pytest.skip(f"Visualizer plugin not available: {e}")
493+
except (VisualizerError, RuntimeError) as e:
494+
pytest.skip(f"Visualizer plugin not available or graphics initialization failed: {e}")
492495
finally:
493496
# Cleanup
494497
if 'visualizer' in locals():
@@ -622,13 +625,13 @@ def test_color_primitives_mock_mode_error(self):
622625
pytest.skip("Visualizer plugin available - not testing mock mode")
623626

624627
# In mock mode, visualizer creation should fail
625-
with pytest.raises(VisualizerError) as exc_info:
628+
with pytest.raises((VisualizerError, RuntimeError)) as exc_info:
626629
Visualizer(400, 300, headless=True)
627630

628631
error_msg = str(exc_info.value).lower()
629-
# Error should mention rebuilding
632+
# Error should mention rebuilding or graphics issues
630633
assert any(keyword in error_msg for keyword in
631-
['rebuild', 'build', 'enable', 'visualizer'])
634+
['rebuild', 'build', 'enable', 'visualizer', 'opengl', 'graphics', 'initialize', 'create'])
632635

633636

634637
if __name__ == "__main__":

0 commit comments

Comments
 (0)