Skip to content

Commit 9b3c4da

Browse files
authored
Merge pull request #5729 from dylanpulver/hex-to-rgb-rejects-invalid-length
Reject hex colors that are not 3 or 6 digits
2 parents ac78b8b + 3aa898c commit 9b3c4da

3 files changed

Lines changed: 44 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
2424
- Fix issue with per-point marker color for hover labels in `scattergl`, `quiver` traces [[#8027](https://github.com/plotly/plotly.js/pull/8027)]
2525
- 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)]
2626
- Note: Safari 15, Chrome 56, Firefox 51 and later are now required for map traces
27+
- 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!
2728

2829
## [7.0.0] - 2026-08-25
2930

_plotly_utils/colors/__init__.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676

7777
import decimal
7878
from numbers import Number
79+
from warnings import warn
7980

8081
from _plotly_utils import exceptions
8182

@@ -763,15 +764,29 @@ def hex_to_rgb(value):
763764
'#FFF' --> (255, 255, 255)
764765
765766
"""
767+
768+
input_value = value
766769
value = value.lstrip("#")
767770
if len(value) == 3:
768771
value = "".join(c * 2 for c in value)
769-
hex_total_length = len(value)
770-
rgb_section_length = hex_total_length // 3
771-
return tuple(
772-
int(value[i : i + rgb_section_length], 16)
773-
for i in range(0, hex_total_length, rgb_section_length)
774-
)
772+
elif len(value) == 4:
773+
warn(
774+
"4-character hex color provided; 4th character will be ignored."
775+
"got {!r}".format(input_value)
776+
)
777+
value = "".join(c * 2 for c in value)[:6]
778+
elif len(value) == 8:
779+
warn(
780+
"8-character hex color provided; last two characters will be ignored."
781+
"got {!r}".format(input_value)
782+
)
783+
value = value[:6]
784+
elif len(value) != 6:
785+
raise ValueError(
786+
"hex color must be 3 or 6 hex digits, optionally prefixed with "
787+
"'#'; got {!r}".format(input_value)
788+
)
789+
return tuple(int(value[i : i + 2], 16) for i in range(0, 6, 2))
775790

776791

777792
def colorscale_to_colors(colorscale):

tests/test_plotly_utils/colors/test_color_conversions.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
13
from _plotly_utils.colors import (
24
find_intermediate_color,
35
hex_to_rgb,
@@ -21,6 +23,26 @@ def test_hex_to_rgb_shorthand_3_digit():
2123
assert hex_to_rgb("#00f") == (0, 0, 255)
2224

2325

26+
@pytest.mark.parametrize("value", ["#abcd", "eb4d", "#12345678", "b24fa3d1"])
27+
def test_hex_to_rgb_warns_on_4_and_8_digits(value):
28+
warning_must_contain = "4-char" if len(value.lstrip("#")) == 4 else "8-char"
29+
with pytest.warns(UserWarning, match=warning_must_contain):
30+
hex_to_rgb(value)
31+
32+
33+
@pytest.mark.parametrize("value", ["#12345", "#1", "#1234567", "", "#"])
34+
def test_hex_to_rgb_rejects_other_lengths(value):
35+
# The section width was len // 3, so "#12345" returned a 5-tuple rather
36+
# than raising.
37+
with pytest.raises(ValueError, match="3 or 6 hex digits"):
38+
hex_to_rgb(value)
39+
40+
41+
def test_hex_to_rgb_accepts_missing_hash():
42+
assert hex_to_rgb("aabbcc") == (170, 187, 204)
43+
assert hex_to_rgb("abc") == (170, 187, 204)
44+
45+
2446
def test_label_rgb_formats_tuple():
2547
assert label_rgb((255, 0, 0)) == "rgb(255, 0, 0)"
2648
assert label_rgb((1, 2, 3)) == "rgb(1, 2, 3)"

0 commit comments

Comments
 (0)