diff --git a/CHANGELOG.md b/CHANGELOG.md index c92c3ea6d64..c0a4c8bb253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fix Plotly Express mutating lists passed to the `x` or `y` arguments in wide mode [[#5727](https://github.com/plotly/plotly.py/pull/5727)], with thanks to @cpruijsen for the contribution! - Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution! - Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura for the contribution! +- Fix graph object properties rejecting NumPy scalars: integer properties (such as `nticks`, `nbinsx` and `updatemenus[].active`) now accept NumPy integers, and string properties (such as `name` and `text`) now accept NumPy numbers, as they already did Python `int` and `float` values [[#5828](https://github.com/plotly/plotly.py/pull/5828)], with thanks to @RaphaelFakhri for the contribution! ## [7.1.0] - 2026-09-15 diff --git a/_plotly_utils/basevalidators.py b/_plotly_utils/basevalidators.py index 1ffc63e5add..e770a759588 100644 --- a/_plotly_utils/basevalidators.py +++ b/_plotly_utils/basevalidators.py @@ -918,7 +918,9 @@ def validate_coerce(self, v): elif self.array_ok and is_simple_array(v): # Check integer type invalid_els = [ - e for e in v if not isinstance(e, int) and e not in self.extras + e + for e in v + if not isinstance(e, numbers.Integral) and e not in self.extras ] if invalid_els: @@ -929,7 +931,10 @@ def validate_coerce(self, v): invalid_els = [ e for e in v - if not (isinstance(e, int) and self.min_val <= e <= self.max_val) + if not ( + isinstance(e, numbers.Integral) + and self.min_val <= e <= self.max_val + ) and e not in self.extras ] @@ -939,7 +944,7 @@ def validate_coerce(self, v): v = to_scalar_or_list(v) else: # Check int - if not isinstance(v, int): + if not isinstance(v, numbers.Integral): # don't let int() cast strings to ints self.raise_invalid_val(v) @@ -1100,7 +1105,7 @@ def validate_coerce(self, v): else: if isinstance(v, str): pass - elif isinstance(v, (int, float)): + elif isinstance(v, numbers.Number): # Convert value to a string v = str(v) else: diff --git a/tests/test_plotly_utils/validators/test_integer_validator.py b/tests/test_plotly_utils/validators/test_integer_validator.py index ef4011ebf2c..4ac6e522fe9 100644 --- a/tests/test_plotly_utils/validators/test_integer_validator.py +++ b/tests/test_plotly_utils/validators/test_integer_validator.py @@ -46,7 +46,9 @@ def validator_extras_aok(): # Acceptance -@pytest.mark.parametrize("val", [1, -19, 0, -1234]) +@pytest.mark.parametrize( + "val", [1, -19, 0, -1234, np.int64(5), np.int32(-3), np.uint8(7)] +) def test_acceptance(val, validator): assert validator.validate_coerce(val) == val @@ -64,7 +66,7 @@ def test_rejection_by_value(val, validator): # With min/max # min == -1 and max == 2 -@pytest.mark.parametrize("val", [0, 1, -1, 2]) +@pytest.mark.parametrize("val", [0, 1, -1, 2, np.int64(2)]) def test_acceptance_min_max(val, validator_min_max): assert validator_min_max.validate_coerce(val) == approx(val) @@ -91,7 +93,7 @@ def test_rejection_extras(val, validator_extras): @pytest.mark.parametrize( - "val", [-1.01, -10, 2.1, 3, np.iinfo(int).max, np.iinfo(int).min] + "val", [-1.01, -10, 2.1, 3, np.int64(3), np.iinfo(int).max, np.iinfo(int).min] ) def test_rejection_min_max(val, validator_min_max): with pytest.raises(ValueError) as validation_failure: @@ -140,7 +142,10 @@ def test_acceptance_aok_scalars(val, validator_aok): assert validator_aok.validate_coerce(val) == val -@pytest.mark.parametrize("val", [[1, 0], [1], [-2, 1, 8], np.array([3, 2, -1, 5])]) +@pytest.mark.parametrize( + "val", + [[1, 0], [1], [-2, 1, 8], [np.int64(3), np.int32(-1)], np.array([3, 2, -1, 5])], +) def test_acceptance_aok_list(val, validator_aok): assert np.array_equal(validator_aok.validate_coerce(val), val) diff --git a/tests/test_plotly_utils/validators/test_string_validator.py b/tests/test_plotly_utils/validators/test_string_validator.py index 7bb59ec3348..af968fcfb76 100644 --- a/tests/test_plotly_utils/validators/test_string_validator.py +++ b/tests/test_plotly_utils/validators/test_string_validator.py @@ -56,7 +56,18 @@ def validator_no_blanks_aok(): # Not strict # Acceptance @pytest.mark.parametrize( - "val", ["bar", 234, np_nan(), "HELLO!!!", "world!@#$%^&*()", "", "\u03bc"] + "val", + [ + "bar", + 234, + np_nan(), + np.int64(234), + np.float32(1.5), + "HELLO!!!", + "world!@#$%^&*()", + "", + "\u03bc", + ], ) def test_acceptance(val, validator): expected = str(val) if not isinstance(val, str) else val