From 039084d08f83599fb315831594bd853f694f8002 Mon Sep 17 00:00:00 2001 From: Jakob Bagterp Date: Sat, 22 Apr 2023 01:54:28 +0200 Subject: [PATCH 1/8] Add new hex abstract class. --- src/colorist/model/abc/hex.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/colorist/model/abc/hex.py diff --git a/src/colorist/model/abc/hex.py b/src/colorist/model/abc/hex.py new file mode 100644 index 00000000..3da5ede0 --- /dev/null +++ b/src/colorist/model/abc/hex.py @@ -0,0 +1,45 @@ +# Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. + +from abc import ABC, abstractmethod + +from ... import helper +from ...constants.ansi import RESET_ALL +from ...helper.error import message_for_hex_value_error +from ...helper.validate import is_valid_hex_value +from ..foreground.rgb import ColorRGB +from .rgb import RGB_ABC + + +class Hex_ABC(ABC): + """Abstract base class for custom hex color instances.""" + + OFF = RESET_ALL + + __slots__ = ["hex", "_rgb", "_ansi_code"] + + def __init__(self, hex: str) -> None: + if not is_valid_hex_value(hex): + raise ValueError(message_for_hex_value_error(hex)) + + self.hex: str = hex + + self._rgb: RGB_ABC = self.convert_hex_to_rgb() + self._ansi_code: str = self.generate_ansi_code() + + def __str__(self) -> str: + return self._ansi_code + + def __repr__(self) -> str: + return f"Hex: #{self.hex.lstrip('#')}" + + def convert_hex_to_rgb(self) -> RGB_ABC: + """Method to convert hex to RGB color.""" + + red, green, blue = helper.convert.hex_to_rgb(self.hex) + return ColorRGB(red, green, blue) + + @abstractmethod + def generate_ansi_code(self) -> str: + """Method to generate ANSI RGB color sequence.""" + + raise NotImplementedError # pragma: no cover From 8b955b2de9ac8bfa465b93f6984a6e15932fd241 Mon Sep 17 00:00:00 2001 From: Jakob Bagterp Date: Sat, 22 Apr 2023 01:55:23 +0200 Subject: [PATCH 2/8] Add classes for ColorHex and BgColorHex, also exposed in the main initialiser. --- src/colorist/__init__.py | 4 +++- src/colorist/model/background/hex.py | 12 ++++++++++++ src/colorist/model/foreground/hex.py | 12 ++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/colorist/model/background/hex.py create mode 100644 src/colorist/model/foreground/hex.py diff --git a/src/colorist/__init__.py b/src/colorist/__init__.py index 42c6e9a2..1cbda497 100644 --- a/src/colorist/__init__.py +++ b/src/colorist/__init__.py @@ -1,7 +1,7 @@ # Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. __all__ = [ - "Color", "BrightColor", "BgColor", "BgBrightColor", "ColorRGB", "BgColorRGB", "ColorHSL", "BgColorHSL", "Effect", + "Color", "BrightColor", "BgColor", "BgBrightColor", "ColorHex", "BgColorHex", "ColorRGB", "BgColorRGB", "ColorHSL", "BgColorHSL", "Effect", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "bright_black", "bright_red", "bright_green", "bright_yellow", "bright_blue", "bright_magenta", "bright_cyan", "bright_white", "bg_black", "bg_red", "bg_green", "bg_yellow", "bg_blue", "bg_magenta", "bg_cyan", "bg_white", @@ -13,11 +13,13 @@ from .model.background.bright_color import BgBrightColor from .model.background.color import BgColor +from .model.background.hex import BgColorHex from .model.background.hsl import BgColorHSL from .model.background.rgb import BgColorRGB from .model.effect import Effect from .model.foreground.bright_color import BrightColor from .model.foreground.color import Color +from .model.foreground.hex import ColorHex from .model.foreground.hsl import ColorHSL from .model.foreground.rgb import ColorRGB from .print.background.bright_color import (bg_bright_black, bg_bright_blue, diff --git a/src/colorist/model/background/hex.py b/src/colorist/model/background/hex.py new file mode 100644 index 00000000..7a7d756b --- /dev/null +++ b/src/colorist/model/background/hex.py @@ -0,0 +1,12 @@ +# Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. + +from ... import helper +from ...constants.ansi import AnsiRgbColorSelector +from ..abc.hex import Hex_ABC + + +class BgColorHex(Hex_ABC): + """Class for custom background color defined as hex color.""" + + def generate_ansi_code(self) -> str: + return helper.generate.ansi_rgb_color_sequence(AnsiRgbColorSelector.BACKGROUND, self._rgb) diff --git a/src/colorist/model/foreground/hex.py b/src/colorist/model/foreground/hex.py new file mode 100644 index 00000000..d12f257a --- /dev/null +++ b/src/colorist/model/foreground/hex.py @@ -0,0 +1,12 @@ +# Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. + +from ... import helper +from ...constants.ansi import AnsiRgbColorSelector +from ..abc.hex import Hex_ABC + + +class ColorHex(Hex_ABC): + """Class for custom foreground text color defined as hex color.""" + + def generate_ansi_code(self) -> str: + return helper.generate.ansi_rgb_color_sequence(AnsiRgbColorSelector.FOREGROUND, self._rgb) From 802cb855d6ae09b9e0e8e19f7748ef02e038a0ac Mon Sep 17 00:00:00 2001 From: Jakob Bagterp Date: Sat, 22 Apr 2023 01:56:51 +0200 Subject: [PATCH 3/8] Add diagram of hex classes. --- src/colorist/model/README.MD | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/colorist/model/README.MD b/src/colorist/model/README.MD index 0515d852..0a6ca80a 100644 --- a/src/colorist/model/README.MD +++ b/src/colorist/model/README.MD @@ -97,3 +97,22 @@ class BgColorHSL HSL_ABC <|-- ColorHSL HSL_ABC <|-- BgColorHSL ``` + +## Hex Color Classes +```mermaid +classDiagram +class Hex_ABC { + <> + +str hex + +ColorRGB _rgb + +str _ansi_code + +str OFF + +convert_hex_to_rgb() + +generate_ansi_code() +} +class ColorHex +class BgColorHex + +Hex_ABC <|-- ColorHex +Hex_ABC <|-- BgColorHex +``` From c79222949e6213b5dba801b9e43d56a8b30e085c Mon Sep 17 00:00:00 2001 From: Jakob Bagterp Date: Sat, 22 Apr 2023 09:39:10 +0200 Subject: [PATCH 4/8] Add tests. --- .../hex/custom_background_hex_color_test.py | 29 +++++++++++++++++++ test/foreground/hex/custom_hex_color_test.py | 29 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 test/background/hex/custom_background_hex_color_test.py create mode 100644 test/foreground/hex/custom_hex_color_test.py diff --git a/test/background/hex/custom_background_hex_color_test.py b/test/background/hex/custom_background_hex_color_test.py new file mode 100644 index 00000000..efaa55f3 --- /dev/null +++ b/test/background/hex/custom_background_hex_color_test.py @@ -0,0 +1,29 @@ +# Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. + +import pytest +import terminal + +from colorist import BgColorHex + +BLACK_MOCK_TEXT = "Only color this \033[48;2;0;0;0mword\033[0m.\n" + +WHITE_MOCK_TEXT = "Only color this \033[48;2;255;255;255mword\033[0m.\n" + + +@pytest.mark.parametrize("hex, expected", [ + ("#000", BLACK_MOCK_TEXT), + ("#000000", BLACK_MOCK_TEXT), + ("000", BLACK_MOCK_TEXT), + ("000000", BLACK_MOCK_TEXT), + ("#fFf", WHITE_MOCK_TEXT), + ("#fFFFFf", WHITE_MOCK_TEXT), + ("fFf", WHITE_MOCK_TEXT), + ("fFFFFf", WHITE_MOCK_TEXT), + ("aB3", "Only color this \033[48;2;170;187;51mword\033[0m.\n"), + ("#9b9Ac7", "Only color this \033[48;2;155;154;199mword\033[0m.\n"), +]) +def test_custom_text_background_hex_color_f_string(hex: str, expected: str, capfd: object) -> None: + bg_color_hex = BgColorHex(hex) + print(f"Only color this {bg_color_hex}word{BgColorHex.OFF}.") + terminal_output = terminal.get_output(capfd) + assert terminal_output == expected diff --git a/test/foreground/hex/custom_hex_color_test.py b/test/foreground/hex/custom_hex_color_test.py new file mode 100644 index 00000000..03f5eefe --- /dev/null +++ b/test/foreground/hex/custom_hex_color_test.py @@ -0,0 +1,29 @@ +# Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. + +import pytest +import terminal + +from colorist import ColorHex + +BLACK_MOCK_TEXT = "Only color this \033[38;2;0;0;0mword\033[0m.\n" + +WHITE_MOCK_TEXT = "Only color this \033[38;2;255;255;255mword\033[0m.\n" + + +@pytest.mark.parametrize("hex, expected", [ + ("#000", BLACK_MOCK_TEXT), + ("#000000", BLACK_MOCK_TEXT), + ("000", BLACK_MOCK_TEXT), + ("000000", BLACK_MOCK_TEXT), + ("#fFf", WHITE_MOCK_TEXT), + ("#fFFFFf", WHITE_MOCK_TEXT), + ("fFf", WHITE_MOCK_TEXT), + ("fFFFFf", WHITE_MOCK_TEXT), + ("aB3", "Only color this \033[38;2;170;187;51mword\033[0m.\n"), + ("#9b9Ac7", "Only color this \033[38;2;155;154;199mword\033[0m.\n"), +]) +def test_custom_text_hex_color_f_string(hex: str, expected: str, capfd: object) -> None: + color_hex = ColorHex(hex) + print(f"Only color this {color_hex}word{ColorHex.OFF}.") + terminal_output = terminal.get_output(capfd) + assert terminal_output == expected From a53e1f9b204b739691e2d218224d0d008dc9606c Mon Sep 17 00:00:00 2001 From: Jakob Bagterp Date: Sat, 22 Apr 2023 10:16:08 +0200 Subject: [PATCH 5/8] Add tests of exception handling for ColorHex and BgColorHex. --- test/model/hex/hex_exception_handling_test.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/model/hex/hex_exception_handling_test.py diff --git a/test/model/hex/hex_exception_handling_test.py b/test/model/hex/hex_exception_handling_test.py new file mode 100644 index 00000000..75e1ada2 --- /dev/null +++ b/test/model/hex/hex_exception_handling_test.py @@ -0,0 +1,41 @@ +# Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. + +from contextlib import nullcontext as does_not_raise +from typing import Any + +import pytest + +from colorist import BgColorHex, ColorHex + +MOCK_DATASET = [ + ("#1AFFa1", does_not_raise()), + ("1AFFa1", does_not_raise()), + ("#1AF", does_not_raise()), + ("1AF", does_not_raise()), + ("#000000", does_not_raise()), + ("000000", does_not_raise()), + ("#000", does_not_raise()), + ("000", does_not_raise()), + ("#FfFFFF", does_not_raise()), + ("FFfFFF", does_not_raise()), + ("#ffF", does_not_raise()), + ("Fff", does_not_raise()), + ("#1AFFa10", pytest.raises(ValueError)), + ("1AFFa10", pytest.raises(ValueError)), + ("#1A", pytest.raises(ValueError)), + ("1A", pytest.raises(ValueError)), + ("#random", pytest.raises(ValueError)), + ("random", pytest.raises(ValueError)), +] + + +@pytest.mark.parametrize("hex, expectation", MOCK_DATASET) +def test_exception_handling_of_color_hex(hex: str, expectation: Any) -> None: + with expectation: + _ = ColorHex(hex) is not None + + +@pytest.mark.parametrize("hex, expectation", MOCK_DATASET) +def test_exception_handling_of_background_color_hex(hex: str, expectation: Any) -> None: + with expectation: + _ = BgColorHex(hex) is not None From 0d469b2cdbcdb846fab394a0500858807dba6c40 Mon Sep 17 00:00:00 2001 From: Jakob Bagterp Date: Sat, 22 Apr 2023 10:24:45 +0200 Subject: [PATCH 6/8] Convert input hex color code to lowercase so it appears consistent in the __repr__ function (no functional changes). --- src/colorist/model/abc/hex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/colorist/model/abc/hex.py b/src/colorist/model/abc/hex.py index 3da5ede0..e4352f88 100644 --- a/src/colorist/model/abc/hex.py +++ b/src/colorist/model/abc/hex.py @@ -21,7 +21,7 @@ def __init__(self, hex: str) -> None: if not is_valid_hex_value(hex): raise ValueError(message_for_hex_value_error(hex)) - self.hex: str = hex + self.hex: str = hex.lower() self._rgb: RGB_ABC = self.convert_hex_to_rgb() self._ansi_code: str = self.generate_ansi_code() From 2397daf554889e215b48dfedc5ae527ec71364ea Mon Sep 17 00:00:00 2001 From: Jakob Bagterp Date: Sat, 22 Apr 2023 10:30:33 +0200 Subject: [PATCH 7/8] Add tests of __repr__ for ColorHex and BgColorHex classes. --- test/model/hex/hex_repr_test.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/model/hex/hex_repr_test.py diff --git a/test/model/hex/hex_repr_test.py b/test/model/hex/hex_repr_test.py new file mode 100644 index 00000000..6e88742c --- /dev/null +++ b/test/model/hex/hex_repr_test.py @@ -0,0 +1,30 @@ +# Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. + +import pytest + +from colorist import BgColorHex, ColorHex + +MOCK_DATASET = [ + ("#000", "Hex: #000"), + ("#000000", "Hex: #000000"), + ("000", "Hex: #000"), + ("000000", "Hex: #000000"), + ("#fFf", "Hex: #fff"), + ("#fFFFFf", "Hex: #ffffff"), + ("fFf", "Hex: #fff"), + ("fFFFFf", "Hex: #ffffff"), + ("aB3", "Hex: #ab3"), + ("#9b9Ac7", "Hex: #9b9ac7"), +] + + +@pytest.mark.parametrize("hex, expected_repr", MOCK_DATASET) +def test_repr_of_color_hex(hex: str, expected_repr: str) -> None: + color_hex = ColorHex(hex) + assert repr(color_hex) == expected_repr + + +@pytest.mark.parametrize("hex, expected_repr", MOCK_DATASET) +def test_repr_of_background_color_hex(hex: str, expected_repr: str) -> None: + color_hex = BgColorHex(hex) + assert repr(color_hex) == expected_repr From c88c7e3b4955a00bf69e6e0d26aae4328f5c271e Mon Sep 17 00:00:00 2001 From: Jakob Bagterp Date: Sat, 22 Apr 2023 10:31:41 +0200 Subject: [PATCH 8/8] Add texts of exception handling for helper.convert.hex_to_rgb() helper method. --- ...vert_hex_to_rgb_exception_handling_test.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/helper/convert/convert_hex_to_rgb_exception_handling_test.py diff --git a/test/helper/convert/convert_hex_to_rgb_exception_handling_test.py b/test/helper/convert/convert_hex_to_rgb_exception_handling_test.py new file mode 100644 index 00000000..452c0b98 --- /dev/null +++ b/test/helper/convert/convert_hex_to_rgb_exception_handling_test.py @@ -0,0 +1,33 @@ +# Copyright 2022 – present, Jakob Bagterp. BSD 3-Clause license and refer to LICENSE file. + +from contextlib import nullcontext as does_not_raise +from typing import Any + +import pytest + +from colorist import helper + + +@pytest.mark.parametrize("hex, expectation", [ + ("#1AFFa1", does_not_raise()), + ("1AFFa1", does_not_raise()), + ("#1AF", does_not_raise()), + ("1AF", does_not_raise()), + ("#000000", does_not_raise()), + ("000000", does_not_raise()), + ("#000", does_not_raise()), + ("000", does_not_raise()), + ("#FfFFFF", does_not_raise()), + ("FFfFFF", does_not_raise()), + ("#ffF", does_not_raise()), + ("Fff", does_not_raise()), + ("#1AFFa10", pytest.raises(ValueError)), + ("1AFFa10", pytest.raises(ValueError)), + ("#1A", pytest.raises(ValueError)), + ("1A", pytest.raises(ValueError)), + ("#random", pytest.raises(ValueError)), + ("random", pytest.raises(ValueError)), +]) +def test_exception_handling_of_convert_hex_to_rgb(hex: str, expectation: Any) -> None: + with expectation: + _ = helper.convert.hex_to_rgb(hex) is not None