Skip to content

Commit 26f86f0

Browse files
committed
Fix BoolItem to convert numpy.bool_ to Python bool
Fixes #96 (cherry picked from commit d4a6a57) # Conflicts: # CHANGELOG.md
1 parent 56a14a0 commit 26f86f0

File tree

4 files changed

+149
-1
lines changed

4 files changed

+149
-1
lines changed

CHANGELOG.md

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

3+
## Version 3.13.4 ##
4+
5+
🛠️ Bug fixes:
6+
7+
* **BoolItem numpy compatibility**: Fixed `numpy.bool_` type conversion issue
8+
* `BoolItem` now ensures all assigned values are converted to Python `bool` type
9+
* Added `__set__` override to convert `numpy.bool_` values to native Python `bool`
10+
* Fixes compatibility issues with Qt APIs that strictly require Python `bool` (e.g., `QAction.setChecked()`)
11+
* Prevents `TypeError: setChecked(self, a0: bool): argument 1 has unexpected type 'numpy.bool'`
12+
* Affects applications using `BoolItem` values with Qt widgets after HDF5 deserialization
13+
* Maintains backward compatibility as `bool(bool)` is a no-op
14+
* This closes [Issue #96](https://github.com/PlotPyStack/guidata/issues/96) - `BoolItem`: `numpy.bool_` compatibility fix
15+
316
## Version 3.13.3 ##
417

518
🛠️ Bug fixes:

guidata/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
and application development tools for Qt.
99
"""
1010

11-
__version__ = "3.13.3"
11+
__version__ = "3.13.4"
1212

1313

1414
# Dear (Debian, RPM, ...) package makers, please feel free to customize the

guidata/dataset/dataitems.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,21 @@ def get_value_from_reader(
653653
statement defined in the base item `deserialize` method"""
654654
return reader.read_bool()
655655

656+
def __set__(self, instance: DataSet, value: bool | None) -> None:
657+
"""Set data item's value, ensuring it's a Python bool
658+
659+
This override ensures that numpy.bool_ values are converted to Python bool,
660+
which is necessary for compatibility with Qt APIs that strictly require
661+
Python bool type (e.g., QAction.setChecked()).
662+
663+
Args:
664+
instance: instance of the DataSet
665+
value: value to set (will be converted to Python bool if not None)
666+
"""
667+
if value is not None:
668+
value = bool(value)
669+
super().__set__(instance, value)
670+
656671

657672
class DateItem(DataItem):
658673
"""DataSet data item
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Test BoolItem with numpy bool types
2+
3+
This test ensures that BoolItem properly converts numpy.bool_ values to Python bool,
4+
which is necessary for compatibility with Qt APIs and other code that strictly requires
5+
Python bool type.
6+
"""
7+
8+
import os
9+
import tempfile
10+
11+
import numpy as np
12+
import pytest
13+
14+
import guidata.dataset as gds
15+
from guidata.io import HDF5Reader, HDF5Writer
16+
17+
18+
class BoolDataSet(gds.DataSet):
19+
"""Test dataset with boolean items"""
20+
21+
bool_true = gds.BoolItem("Boolean True", default=True)
22+
bool_false = gds.BoolItem("Boolean False", default=False)
23+
bool_none = gds.BoolItem("Boolean None", default=None, allow_none=True)
24+
25+
26+
class TestBoolItemNumpy:
27+
"""Test BoolItem with numpy bool types"""
28+
29+
def test_numpy_bool_assignment(self):
30+
"""Test that assigning numpy.bool_ is converted to Python bool"""
31+
ds = BoolDataSet()
32+
33+
# Test True
34+
ds.bool_true = np.bool_(True)
35+
assert ds.bool_true is True
36+
assert type(ds.bool_true) is bool
37+
38+
# Test False
39+
ds.bool_false = np.bool_(False)
40+
assert ds.bool_false is False
41+
assert type(ds.bool_false) is bool
42+
43+
def test_python_bool_assignment(self):
44+
"""Test that assigning Python bool still works"""
45+
ds = BoolDataSet()
46+
47+
# Test True
48+
ds.bool_true = True
49+
assert ds.bool_true is True
50+
assert type(ds.bool_true) is bool
51+
52+
# Test False
53+
ds.bool_false = False
54+
assert ds.bool_false is False
55+
assert type(ds.bool_false) is bool
56+
57+
def test_none_assignment(self):
58+
"""Test that None assignment works when allow_none=True"""
59+
ds = BoolDataSet()
60+
61+
ds.bool_none = None
62+
assert ds.bool_none is None
63+
64+
def test_hdf5_serialization(self):
65+
"""Test that HDF5 serialization/deserialization maintains Python bool type"""
66+
ds = BoolDataSet()
67+
ds.bool_true = True
68+
ds.bool_false = False
69+
70+
with tempfile.NamedTemporaryFile(suffix=".h5", delete=False) as tmp:
71+
tmp_path = tmp.name
72+
73+
try:
74+
# Write
75+
with HDF5Writer(tmp_path) as writer:
76+
writer.write(ds, group_name="test")
77+
78+
# Read back
79+
ds2 = BoolDataSet()
80+
with HDF5Reader(tmp_path) as reader:
81+
reader.read("test", instance=ds2)
82+
83+
# Verify types
84+
assert type(ds2.bool_true) is bool
85+
assert ds2.bool_true is True
86+
87+
assert type(ds2.bool_false) is bool
88+
assert ds2.bool_false is False
89+
90+
finally:
91+
os.unlink(tmp_path)
92+
93+
def test_numpy_bool_after_deserialization(self):
94+
"""Test that numpy.bool_ assignment works after HDF5 deserialization"""
95+
ds = BoolDataSet()
96+
ds.bool_true = True
97+
98+
with tempfile.NamedTemporaryFile(suffix=".h5", delete=False) as tmp:
99+
tmp_path = tmp.name
100+
101+
try:
102+
# Write and read
103+
with HDF5Writer(tmp_path) as writer:
104+
writer.write(ds, group_name="test")
105+
106+
ds2 = BoolDataSet()
107+
with HDF5Reader(tmp_path) as reader:
108+
reader.read("test", instance=ds2)
109+
110+
# Now assign numpy.bool_ and verify it's converted
111+
ds2.bool_true = np.bool_(False)
112+
assert type(ds2.bool_true) is bool
113+
assert ds2.bool_true is False
114+
115+
finally:
116+
os.unlink(tmp_path)
117+
118+
119+
if __name__ == "__main__":
120+
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)