From 1a9699a0345738b1811f08787c0e10a5442d13d8 Mon Sep 17 00:00:00 2001 From: damusss Date: Tue, 11 Mar 2025 15:49:59 +0100 Subject: [PATCH 1/4] Add Color.hex --- buildconfig/stubs/pygame/color.pyi | 1 + docs/reST/ref/color.rst | 17 +++++++++++++++ src_c/color.c | 33 ++++++++++++++++++++++++++++++ src_c/doc/color_doc.h | 1 + test/color_test.py | 33 +++++++++++++++++++++++++++++- 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/buildconfig/stubs/pygame/color.pyi b/buildconfig/stubs/pygame/color.pyi index 1c69ab6083..d07948965e 100644 --- a/buildconfig/stubs/pygame/color.pyi +++ b/buildconfig/stubs/pygame/color.pyi @@ -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: ... diff --git a/docs/reST/ref/color.rst b/docs/reST/ref/color.rst index 4d36c23fcc..2683d31cef 100644 --- a/docs/reST/ref/color.rst +++ b/docs/reST/ref/color.rst @@ -210,6 +210,23 @@ .. ## Color.normalized ## + .. attribute:: hex + + | :sl:`Gets or sets the stringified hexadecimal representation of the Color.` + | :sg:`hex -> str` + + The strigified hexadecimal representation of the Color. The hexadecimal string + is formatted as ``"#rrggbbaa"`` where rr, gg, bb, and aa are 2 digits 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` diff --git a/src_c/color.c b/src_c/color.c index baea8bb96c..4c8ef69d23 100644 --- a/src_c/color.c +++ b/src_c/color.c @@ -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 */ @@ -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}}; @@ -1543,6 +1549,33 @@ _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) +{ + 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) { diff --git a/src_c/doc/color_doc.h b/src_c/doc/color_doc.h index 1a469e4a66..a16ba2076b 100644 --- a/src_c/doc/color_doc.h +++ b/src_c/doc/color_doc.h @@ -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" diff --git a/test/color_test.py b/test/color_test.py index deca70957d..b5a65bc09d 100644 --- a/test/color_test.py +++ b/test/color_test.py @@ -950,6 +950,31 @@ 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 + + for value in ["FFFFFFFF", "#FFzzFF00", "0x FFFFFF", "#FF"]: + with self.assertRaises(ValueError): + color.hex = value + + for value in ["#FFFFFFFF", "#FFFFFF", "0xFFFFFFFF", "0xFFFFFF"]: + color.hex = value + 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 @@ -1007,6 +1032,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): @@ -1021,7 +1049,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: @@ -1042,6 +1070,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"): From 499c58ade04462d0a6ddabb2e2186c2971fe2161 Mon Sep 17 00:00:00 2001 From: damusss Date: Tue, 11 Mar 2025 20:28:31 +0100 Subject: [PATCH 2/4] Add DEL_ATTR_NOT_SUPPORTED_CHECK --- src_c/color.c | 3 +++ test/color_test.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src_c/color.c b/src_c/color.c index 4c8ef69d23..9f71e9a0c0 100644 --- a/src_c/color.c +++ b/src_c/color.c @@ -1560,10 +1560,13 @@ _color_get_hex(pgColorObject *color, void *closure) 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"); diff --git a/test/color_test.py b/test/color_test.py index b5a65bc09d..f1f7216db5 100644 --- a/test/color_test.py +++ b/test/color_test.py @@ -966,6 +966,8 @@ def test_hex_property(self): with self.assertRaises(TypeError): color.hex = 0xFFFFFFFF + with self.assertRaises(AttributeError): + del color.hex for value in ["FFFFFFFF", "#FFzzFF00", "0x FFFFFF", "#FF"]: with self.assertRaises(ValueError): From 8706170dc90f89347108728233da8952387398b2 Mon Sep 17 00:00:00 2001 From: damusss Date: Mon, 24 Mar 2025 22:35:57 +0100 Subject: [PATCH 3/4] Fix docs and tests --- docs/reST/ref/color.rst | 4 ++-- test/color_test.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/reST/ref/color.rst b/docs/reST/ref/color.rst index 2683d31cef..2198f7492b 100644 --- a/docs/reST/ref/color.rst +++ b/docs/reST/ref/color.rst @@ -215,8 +215,8 @@ | :sl:`Gets or sets the stringified hexadecimal representation of the Color.` | :sg:`hex -> str` - The strigified hexadecimal representation of the Color. The hexadecimal string - is formatted as ``"#rrggbbaa"`` where rr, gg, bb, and aa are 2 digits hex numbers + 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 diff --git a/test/color_test.py b/test/color_test.py index f1f7216db5..116e241dbf 100644 --- a/test/color_test.py +++ b/test/color_test.py @@ -970,12 +970,14 @@ def test_hex_property(self): del color.hex for value in ["FFFFFFFF", "#FFzzFF00", "0x FFFFFF", "#FF"]: - with self.assertRaises(ValueError): - color.hex = value + for v in [value, value.lower()]: + with self.assertRaises(ValueError): + color.hex = v for value in ["#FFFFFFFF", "#FFFFFF", "0xFFFFFFFF", "0xFFFFFF"]: - color.hex = value - self.assertEqual((color.r, color.g, color.b, color.a), (255, 255, 255, 255)) + 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 From a44c2eaf012a939be63b88c9944571178dc2d1e7 Mon Sep 17 00:00:00 2001 From: Matiiss <83066658+Matiiss@users.noreply.github.com> Date: Tue, 25 Mar 2025 18:25:17 +0200 Subject: [PATCH 4/4] Formatting This little maneuver took 15 hours! on my Android --- test/color_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/color_test.py b/test/color_test.py index 116e241dbf..ebf3cf12f6 100644 --- a/test/color_test.py +++ b/test/color_test.py @@ -977,7 +977,9 @@ def test_hex_property(self): 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)) + 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