From 1027e03ff6ed54aa49945a0516f1574995197418 Mon Sep 17 00:00:00 2001 From: Dylan Pulver Date: Mon, 14 Sep 2026 10:21:31 +0200 Subject: [PATCH 1/4] Reject hex colors that are not 3 or 6 digits hex_to_rgb computed the section width as len(value) // 3 and sliced in steps of that width, so "#12345" came back as the 5-tuple (1, 2, 3, 4, 5) rather than being rejected. An empty string raised "range() arg 3 must not be zero", which says nothing about colors. The docstring already gives the contract: 3 or 6 digits. This enforces it and keeps the two valid shapes working, with or without the leading '#'. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011M5uTyCU4WcNTsPvGrErDo --- _plotly_utils/colors/__init__.py | 14 ++++++++------ .../colors/test_color_conversions.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/_plotly_utils/colors/__init__.py b/_plotly_utils/colors/__init__.py index e3b5cfa3205..53fa9ade91d 100644 --- a/_plotly_utils/colors/__init__.py +++ b/_plotly_utils/colors/__init__.py @@ -766,12 +766,14 @@ def hex_to_rgb(value): value = value.lstrip("#") if len(value) == 3: value = "".join(c * 2 for c in value) - hex_total_length = len(value) - rgb_section_length = hex_total_length // 3 - return tuple( - int(value[i : i + rgb_section_length], 16) - for i in range(0, hex_total_length, rgb_section_length) - ) + elif len(value) != 6: + # Any other length silently produced a tuple of the wrong size: the + # section width is len // 3, so "#12345" returned a 5-tuple. + raise ValueError( + "hex color must be 3 or 6 hex digits, optionally prefixed with " + "'#'; got {!r}".format(value) + ) + return tuple(int(value[i : i + 2], 16) for i in range(0, 6, 2)) def colorscale_to_colors(colorscale): diff --git a/tests/test_plotly_utils/colors/test_color_conversions.py b/tests/test_plotly_utils/colors/test_color_conversions.py index 6513b7c3233..97329a738ca 100644 --- a/tests/test_plotly_utils/colors/test_color_conversions.py +++ b/tests/test_plotly_utils/colors/test_color_conversions.py @@ -1,3 +1,5 @@ +import pytest + from _plotly_utils.colors import ( find_intermediate_color, hex_to_rgb, @@ -21,6 +23,19 @@ def test_hex_to_rgb_shorthand_3_digit(): assert hex_to_rgb("#00f") == (0, 0, 255) +@pytest.mark.parametrize("value", ["#12345", "#1", "#1234567", "", "#"]) +def test_hex_to_rgb_rejects_other_lengths(value): + # The section width was len // 3, so "#12345" returned a 5-tuple rather + # than raising. + with pytest.raises(ValueError, match="3 or 6 hex digits"): + hex_to_rgb(value) + + +def test_hex_to_rgb_accepts_missing_hash(): + assert hex_to_rgb("aabbcc") == (170, 187, 204) + assert hex_to_rgb("abc") == (170, 187, 204) + + def test_label_rgb_formats_tuple(): assert label_rgb((255, 0, 0)) == "rgb(255, 0, 0)" assert label_rgb((1, 2, 3)) == "rgb(1, 2, 3)" From 80a94ecef7c18e35097883655cddb83b3beb2c9d Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:35:39 -0400 Subject: [PATCH 2/4] accept 4- and 8- character hex codes with a warning --- _plotly_utils/colors/__init__.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/_plotly_utils/colors/__init__.py b/_plotly_utils/colors/__init__.py index 53fa9ade91d..a9689101e6b 100644 --- a/_plotly_utils/colors/__init__.py +++ b/_plotly_utils/colors/__init__.py @@ -76,6 +76,7 @@ import decimal from numbers import Number +from warnings import warn from _plotly_utils import exceptions @@ -763,15 +764,27 @@ def hex_to_rgb(value): '#FFF' --> (255, 255, 255) """ + + input_value = value value = value.lstrip("#") if len(value) == 3: value = "".join(c * 2 for c in value) + elif len(value) == 4: + warn( + "4-character hex color provided; 4th character will be ignored." + "got {!r}".format(input_value) + ) + value = "".join(c * 2 for c in value)[:6] + elif len(value) == 8: + warn( + "8-character hex color provided; last two characters will be ignored." + "got {!r}".format(input_value) + ) + value = value[:6] elif len(value) != 6: - # Any other length silently produced a tuple of the wrong size: the - # section width is len // 3, so "#12345" returned a 5-tuple. raise ValueError( "hex color must be 3 or 6 hex digits, optionally prefixed with " - "'#'; got {!r}".format(value) + "'#'; got {!r}".format(input_value) ) return tuple(int(value[i : i + 2], 16) for i in range(0, 6, 2)) From 7c85d48e0b3cd4ced88fcd44bc0770388f1c501c Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:36:05 -0400 Subject: [PATCH 3/4] add test for hex_to_rgb 4- and 8-character hex codes --- tests/test_plotly_utils/colors/test_color_conversions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_plotly_utils/colors/test_color_conversions.py b/tests/test_plotly_utils/colors/test_color_conversions.py index 97329a738ca..8e28734b045 100644 --- a/tests/test_plotly_utils/colors/test_color_conversions.py +++ b/tests/test_plotly_utils/colors/test_color_conversions.py @@ -23,6 +23,13 @@ def test_hex_to_rgb_shorthand_3_digit(): assert hex_to_rgb("#00f") == (0, 0, 255) +@pytest.mark.parametrize("value", ["#abcd", "eb4d", "#12345678", "b24fa3d1"]) +def test_hex_to_rgb_warns_on_4_and_8_digits(value): + warning_must_contain = "4-char" if len(value.lstrip("#")) == 4 else "8-char" + with pytest.warns(UserWarning, match=warning_must_contain): + hex_to_rgb(value) + + @pytest.mark.parametrize("value", ["#12345", "#1", "#1234567", "", "#"]) def test_hex_to_rgb_rejects_other_lengths(value): # The section width was len // 3, so "#12345" returned a 5-tuple rather From a93dcc05ca3262524331da25a5f9a55aae86e4fe Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:42:01 -0400 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b2594c9e3..a5c9e070ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fix issue with per-point marker color for hover labels in `scattergl`, `quiver` traces [[#8027](https://github.com/plotly/plotly.js/pull/8027)] - Update `maplibre-gl` to v6 to address [CVE-2026-85061](https://github.com/advisories/GHSA-jrc7-96c5-q579) [[#8035](https://github.com/plotly/plotly.js/pull/8035)] - Note: Safari 15, Chrome 56, Firefox 51 and later are now required for map traces +- Update `hex_to_rgb` function to raise error for invalid-length hex codes, and emit warning for hex codes containing alpha [[#5729](https://github.com/plotly/plotly.py/pull/5729)], with thanks to @dylanpulver for the contribution! ## [7.0.0] - 2026-08-25