Skip to content

Add Color.hex #3379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions buildconfig/stubs/pygame/color.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Color(Collection[int]):
hsla: tuple[float, float, float, float]
i1i2i3: tuple[float, float, float]
normalized: tuple[float, float, float, float]
hex: str
__hash__: ClassVar[None] # type: ignore[assignment]
@property
def __array_struct__(self) -> Any: ...
Expand Down
17 changes: 17 additions & 0 deletions docs/reST/ref/color.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,23 @@

.. ## Color.normalized ##

.. attribute:: hex

| :sl:`Gets or sets the stringified hexadecimal representation of the Color.`
| :sg:`hex -> str`

The stringified hexadecimal representation of the Color. The hexadecimal string
is formatted as ``"#rrggbbaa"`` where rr, gg, bb, and aa are two digit hex numbers
in the range from 0x00 to 0xff.

Setting this property means changing the color channels in place. Both lowercase
and uppercase letters are allowed, the alpha can be omitted (defaults to 0xff) and
the string can start with either ``#`` or ``0x``.

.. versionadded:: 2.5.4

.. ## Color.hex ##

.. classmethod:: from_cmy

| :sl:`Returns a Color object from a CMY representation`
Expand Down
36 changes: 36 additions & 0 deletions src_c/color.c
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ _color_get_normalized(pgColorObject *, void *);
static int
_color_set_normalized(pgColorObject *, PyObject *, void *);
static PyObject *
_color_get_hex(pgColorObject *, void *);
static int
_color_set_hex(pgColorObject *, PyObject *, void *);
static PyObject *
_color_get_arraystruct(pgColorObject *, void *);

/* Number protocol methods */
Expand Down Expand Up @@ -269,6 +273,8 @@ static PyGetSetDef _color_getsets[] = {
NULL},
{"normalized", (getter)_color_get_normalized,
(setter)_color_set_normalized, DOC_COLOR_NORMALIZED, NULL},
{"hex", (getter)_color_get_hex, (setter)_color_set_hex, DOC_COLOR_HEX,
NULL},
{"__array_struct__", (getter)_color_get_arraystruct, NULL,
"array structure interface, read only", NULL},
{NULL, NULL, NULL, NULL, NULL}};
Expand Down Expand Up @@ -1543,6 +1549,36 @@ _color_set_normalized(pgColorObject *color, PyObject *value, void *closure)
return 0;
}

static PyObject *
_color_get_hex(pgColorObject *color, void *closure)
{
return PyUnicode_FromFormat("#%02x%02x%02x%02x", color->data[0],
color->data[1], color->data[2],
color->data[3]);
}

static int
_color_set_hex(pgColorObject *color, PyObject *value, void *closure)
{
DEL_ATTR_NOT_SUPPORTED_CHECK("hex", value);

if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "hex color must be a string");
return -1;
}

switch (_hexcolor(value, color->data)) {
case TRISTATE_FAIL:
PyErr_SetString(PyExc_ValueError, "invalid hex string");
return -1;
case TRISTATE_ERROR:
return -1; /* forward python error */
default:
return 0;
}
return 0;
}

static PyObject *
_color_get_arraystruct(pgColorObject *color, void *closure)
{
Expand Down
1 change: 1 addition & 0 deletions src_c/doc/color_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#define DOC_COLOR_HSLA "hsla -> tuple\nGets or sets the HSLA representation of the Color."
#define DOC_COLOR_I1I2I3 "i1i2i3 -> tuple\nGets or sets the I1I2I3 representation of the Color."
#define DOC_COLOR_NORMALIZED "normalized -> tuple\nGets or sets the normalized representation of the Color."
#define DOC_COLOR_HEX "hex -> str\nGets or sets the stringified hexadecimal representation of the Color."
#define DOC_COLOR_FROMCMY "from_cmy(object, /) -> Color\nfrom_cmy(c, m, y, /) -> Color\nReturns a Color object from a CMY representation"
#define DOC_COLOR_FROMHSVA "from_hsva(object, /) -> Color\nfrom_hsva(h, s, v, a, /) -> Color\nReturns a Color object from an HSVA representation"
#define DOC_COLOR_FROMHSLA "from_hsla(object, /) -> Color\nfrom_hsla(h, s, l, a, /) -> Color\nReturns a Color object from an HSLA representation"
Expand Down
39 changes: 38 additions & 1 deletion test/color_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,37 @@ def test_normalize__all_elements_within_limits(self):
self.assertTrue(0 <= b <= 1)
self.assertTrue(0 <= a <= 1)

def test_hex_property(self):
color = pygame.Color(255, 0, 255, 0)
hex = color.hex
self.assertEqual(hex, "#ff00ff00")

for c in rgba_combos_Color_generator():
col_hex = c.hex
self.assertIsInstance(col_hex, str)
self.assertEqual(len(col_hex), 9)
self.assertEqual(col_hex[0], "#")
for char in col_hex:
self.assertIn(char, "#0123456789abcdef")
self.assertEqual(c, pygame.Color(col_hex))

with self.assertRaises(TypeError):
color.hex = 0xFFFFFFFF
with self.assertRaises(AttributeError):
del color.hex

for value in ["FFFFFFFF", "#FFzzFF00", "0x FFFFFF", "#FF"]:
for v in [value, value.lower()]:
with self.assertRaises(ValueError):
color.hex = v

for value in ["#FFFFFFFF", "#FFFFFF", "0xFFFFFFFF", "0xFFFFFF"]:
for v in [value, value.lower()]:
color.hex = v
self.assertEqual(
(color.r, color.g, color.b, color.a), (255, 255, 255, 255)
)

def test_issue_284(self):
"""PyColor OverflowError on HSVA with hue value of 360

Expand Down Expand Up @@ -1007,6 +1038,9 @@ def test_i1i2i3__sanity_testing_converted_should_not_raise(self):
def test_normalized__sanity_testing_converted_should_not_raise(self):
self.colorspaces_converted_should_not_raise("normalized")

def test_hex__sanity_testing_converted_should_not_raise(self):
self.colorspaces_converted_should_not_raise("hex")

################################################################################

def colorspaces_converted_should_equate_bar_rounding(self, prop):
Expand All @@ -1021,7 +1055,7 @@ def colorspaces_converted_should_equate_bar_rounding(self, prop):
self.assertTrue(abs(other.b - c.b) <= 1)
self.assertTrue(abs(other.g - c.g) <= 1)
# CMY and I1I2I3 do not care about the alpha
if not prop in ("cmy", "i1i2i3"):
if prop not in ("cmy", "i1i2i3"):
self.assertTrue(abs(other.a - c.a) <= 1)

except ValueError:
Expand All @@ -1042,6 +1076,9 @@ def test_i1i2i3__sanity_testing_converted_should_equate_bar_rounding(self):
def test_normalized__sanity_testing_converted_should_equate_bar_rounding(self):
self.colorspaces_converted_should_equate_bar_rounding("normalized")

def test_hex__sanity_testing_converted_should_equate_bar_rounding(self):
self.colorspaces_converted_should_equate_bar_rounding("hex")

def test_colorspaces_deprecated_large_sequence(self):
c = pygame.Color("black")
for space in ("hsla", "hsva", "i1i2i3", "cmy", "normalized"):
Expand Down
Loading