Skip to content

Commit

Permalink
Merge pull request #141 from jakob-bagterp/feature/add-hex-class-for-…
Browse files Browse the repository at this point in the history
…custom-text-color

Feature: Add hex classes ColorHex and BgColorHex for custom text color
  • Loading branch information
jakob-bagterp authored Apr 22, 2023
2 parents eac3133 + c88c7e3 commit 73b2468
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/colorist/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions src/colorist/model/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,22 @@ class BgColorHSL
HSL_ABC <|-- ColorHSL
HSL_ABC <|-- BgColorHSL
```

## Hex Color Classes
```mermaid
classDiagram
class Hex_ABC {
<<abstract>>
+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
```
45 changes: 45 additions & 0 deletions src/colorist/model/abc/hex.py
Original file line number Diff line number Diff line change
@@ -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.lower()

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
12 changes: 12 additions & 0 deletions src/colorist/model/background/hex.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions src/colorist/model/foreground/hex.py
Original file line number Diff line number Diff line change
@@ -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)
29 changes: 29 additions & 0 deletions test/background/hex/custom_background_hex_color_test.py
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions test/foreground/hex/custom_hex_color_test.py
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions test/helper/convert/convert_hex_to_rgb_exception_handling_test.py
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions test/model/hex/hex_exception_handling_test.py
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions test/model/hex/hex_repr_test.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 73b2468

Please sign in to comment.