From 20dfcb5c6292cda069a7ec950c349d5a66e99ccf Mon Sep 17 00:00:00 2001 From: delameter Date: Sat, 16 Apr 2022 22:55:26 +0300 Subject: [PATCH] Removed excessive _EmptySequenceSGR_: default _SGR_ class without params was specifically implemented to print out as empty string instead of `\e[m`. --- .env.dist | 2 +- Makefile | 2 +- README.md | 15 ++++++---- setup.cfg | 2 +- src/pytermor/fmt.py | 22 +++++++-------- src/pytermor/registry.py | 4 +-- src/pytermor/seq.py | 60 ++++++++++++++-------------------------- tests/test_fmt.py | 10 +++---- tests/test_seq.py | 58 ++++++++++++++++++++++++++------------ 9 files changed, 91 insertions(+), 84 deletions(-) diff --git a/.env.dist b/.env.dist index f380d6f1..9168a71e 100644 --- a/.env.dist +++ b/.env.dist @@ -1,3 +1,3 @@ -VERSION=1.4.0 +VERSION=1.5.0 PYPI_USERNAME=__token__ PYPI_PASSWORD= #api token diff --git a/Makefile b/Makefile index 8105226c..833c7b90 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ prepare: python3 -m pip install --upgrade build twine test: ## Run tests - PYTHONPATH=src python3 -m unittest + PYTHONPATH=src python3 -m unittest -v set-version: ## Set new package version @echo "Current version: ${YELLOW}${VERSION}${RESET}" diff --git a/README.md b/README.md index fdcca5ad..33ba3ad5 100644 --- a/README.md +++ b/README.md @@ -106,14 +106,14 @@ print(msg) There are two ways to manage color and attribute termination: -- hard reset (SGR 0 | `\e[m`) +- hard reset (SGR 0 | `\e[0m`) - soft reset (SGR 22, 23, 24 etc.) The main difference between them is that **hard** reset disables all formatting after itself, while **soft** reset disables only actually necessary attributes (i.e. used as opening sequence in _Format_ instance's context) and keeps the other. That's what _Format_ class and `autof` method are designed for: to simplify creation of soft-resetting text spans, so that developer doesn't have to restore all previously applied formats after every closing sequence. -Example: we are given a text span which is initially **bold** and underlined. We want to recolor a few words inside of this span. By default this will result in losing all the formatting to the right of updated text span (because `RESET`|`\e[m` clears all text attributes). +Example: we are given a text span which is initially **bold** and underlined. We want to recolor a few words inside of this span. By default this will result in losing all the formatting to the right of updated text span (because `RESET`|`\e[0m` clears all text attributes). However, there is an option to specify what attributes should be disabled or let the library do that for you: @@ -143,7 +143,7 @@ As you can see, the update went well — we kept all the previously applied ### autof -Signature: `autof(*params str|int|AbstractSequenceSGR) -> Format` +Signature: `autof(*params str|int|SequenceSGR) -> Format` Create new _Format_ with specified control sequence(s) as a opening/starter sequence and **automatically compose** closing sequence that will terminate attributes defined in opening sequence while keeping the others (soft reset). @@ -153,15 +153,14 @@ Each sequence param can be specified as: - string key (see [API: Registries](#api-registries)) - integer param value - existing _SequenceSGR_ instance (params will be extracted) -- another inheritor of _AbstractSequenceSGR_: _EmptySequenceSGR_ ### build -Signature: `build(*params str|int|AbstractSequenceSGR) -> AbstractSequenceSGR` +Signature: `build(*params str|int|SequenceSGR) -> SequenceSGR` Create new _SequenceSGR_ with specified params. Resulting sequence params order is the same as argument order. Parameter specification is the same as for `autof`. -Will create _EmptySequenceSGR_ when called without arguments. The purpose of this class is to serve as an empty placeholder when no SGR params are specified; without it method would have created `SequenceSGR()`, which translates to `\e[m`, hard reset sequence, and that's highly counter-intuitive. +_SequenceSGR_ with zero params was specifically implemented to translate into empty string and not into `\e[m`, which wolud make sense, but also would be very entangling, as it's equivavlent of `\e[0m` — **hard reset** sequence. ### build_c256 @@ -833,6 +832,10 @@ You can of course create your own sequences and formats, but with one limitation ## Changelog +### v1.5.0 + +- Removed excessive _EmptySequenceSGR_ — default _SGR_ class without params was specifically implemented to print out as empty string instead of `\e[m`. + ### v1.4.0 - `Format.wrap()` now accepts any type of argument, not only _str_. diff --git a/setup.cfg b/setup.cfg index 9c8a4c60..170f234c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pytermor -version = 1.4.0 +version = 1.5.0 author = Aleksandr Shavykin author_email = 0.delameter@gmail.com description = ANSI formatted terminal output library diff --git a/src/pytermor/fmt.py b/src/pytermor/fmt.py index 045f6188..b983adc3 100644 --- a/src/pytermor/fmt.py +++ b/src/pytermor/fmt.py @@ -9,7 +9,7 @@ from . import build, code from .registry import sgr_parity_registry -from .seq import AbstractSequenceSGR, SequenceSGR +from .seq import SequenceSGR class AbstractFormat(metaclass=ABCMeta): @@ -32,12 +32,12 @@ def closing_str(self) -> str: @property @abstractmethod - def opening_seq(self) -> AbstractSequenceSGR|None: + def opening_seq(self) -> SequenceSGR | None: raise NotImplementedError @property @abstractmethod - def closing_seq(self) -> AbstractSequenceSGR|None: + def closing_seq(self) -> SequenceSGR | None: raise NotImplementedError def __repr__(self): @@ -59,18 +59,18 @@ def closing_str(self) -> str: return '' @property - def opening_seq(self) -> AbstractSequenceSGR|None: + def opening_seq(self) -> SequenceSGR | None: return None @property - def closing_seq(self) -> AbstractSequenceSGR|None: + def closing_seq(self) -> SequenceSGR | None: return None class Format(AbstractFormat): - def __init__(self, opening_seq: AbstractSequenceSGR, closing_seq: AbstractSequenceSGR = None, hard_reset_after: bool = False): - self._opening_seq: AbstractSequenceSGR = opening_seq - self._closing_seq: AbstractSequenceSGR|None = closing_seq + def __init__(self, opening_seq: SequenceSGR, closing_seq: SequenceSGR = None, hard_reset_after: bool = False): + self._opening_seq: SequenceSGR = opening_seq + self._closing_seq: SequenceSGR | None = closing_seq if hard_reset_after: self._closing_seq = SequenceSGR(0) @@ -87,7 +87,7 @@ def opening_str(self) -> str: return self._opening_seq.print() @property - def opening_seq(self) -> AbstractSequenceSGR: + def opening_seq(self) -> SequenceSGR: return self._opening_seq @property @@ -97,7 +97,7 @@ def closing_str(self) -> str: return '' @property - def closing_seq(self) -> AbstractSequenceSGR|None: + def closing_seq(self) -> SequenceSGR | None: return self._closing_seq def __eq__(self, other: Format) -> bool: @@ -111,7 +111,7 @@ def __repr__(self): return super().__repr__() + '[{!r}, {!r}]'.format(self._opening_seq, self._closing_seq) -def autof(*args: str|int|AbstractSequenceSGR) -> Format: +def autof(*args: str | int | SequenceSGR) -> Format: opening_seq = build(*args) closing_seq = sgr_parity_registry.get_closing_seq(opening_seq) return Format(opening_seq, closing_seq) diff --git a/src/pytermor/registry.py b/src/pytermor/registry.py index 5a974d56..4d386ca1 100644 --- a/src/pytermor/registry.py +++ b/src/pytermor/registry.py @@ -10,7 +10,7 @@ from pytermor import build from . import code -from .seq import SequenceSGR, AbstractSequenceSGR +from .seq import SequenceSGR class Registry: @@ -32,7 +32,7 @@ def register_complex(self, starter_codes: Tuple[int, ...], param_len: int, break self._complex_code_def[starter_codes] = param_len self._complex_code_max_len = max(self._complex_code_max_len, len(starter_codes) + param_len) - def get_closing_seq(self, opening_seq: AbstractSequenceSGR) -> AbstractSequenceSGR: + def get_closing_seq(self, opening_seq: SequenceSGR) -> SequenceSGR: closing_seq_params: List[int] = [] opening_params = copy(opening_seq.params) while len(opening_params): diff --git a/src/pytermor/seq.py b/src/pytermor/seq.py index ab66023c..e5d9472f 100644 --- a/src/pytermor/seq.py +++ b/src/pytermor/seq.py @@ -12,17 +12,18 @@ class AbstractSequence(metaclass=ABCMeta): def __init__(self, *params: int): - self._params: List[int] = [int(p) for p in params] + self._params: List[int] = [max(0, int(p)) for p in params] @abstractmethod - def print(self) -> str: raise NotImplementedError + def print(self) -> str: + raise NotImplementedError @property def params(self) -> List[int]: return self._params def __eq__(self, other: AbstractSequence): - if not isinstance(other, type(self)): + if type(self) != type(other): return False return self._params == other._params @@ -42,57 +43,41 @@ def __str__(self) -> str: return self.print() -class AbstractSequenceSGR(AbstractSequenceCSI, metaclass=ABCMeta): +class SequenceSGR(AbstractSequenceCSI, metaclass=ABCMeta): TERMINATOR = 'm' - def __add__(self, other: AbstractSequenceSGR) -> AbstractSequenceSGR: + def print(self) -> str: + if len(self._params) == 0: + return '' + return f'{self.CONTROL_CHARACTER}' \ + f'{self.INTRODUCER}' \ + f'{self.SEPARATOR.join([str(param) for param in self._params])}' \ + f'{self.TERMINATOR}' + + def __add__(self, other: SequenceSGR) -> SequenceSGR: self._ensure_sequence(other) return SequenceSGR(*self._params, *other._params) - def __radd__(self, other: AbstractSequenceSGR) -> AbstractSequenceSGR: + def __radd__(self, other: SequenceSGR) -> SequenceSGR: return other.__add__(self) - def __iadd__(self, other: AbstractSequenceSGR) -> AbstractSequenceSGR: + def __iadd__(self, other: SequenceSGR) -> SequenceSGR: return self.__add__(other) - def __eq__(self, other: AbstractSequenceSGR): - self._ensure_sequence(other) + def __eq__(self, other: SequenceSGR): if type(self) != type(other): return False return self._params == other._params # noinspection PyMethodMayBeStatic def _ensure_sequence(self, subject: Any): - if not isinstance(subject, AbstractSequenceSGR): + if not isinstance(subject, SequenceSGR): raise TypeError( - f'Expected AbstractSequenceSGR, got {type(subject)}' + f'Expected SequenceSGR, got {type(subject)}' ) -class EmptySequenceSGR(AbstractSequenceSGR): - def __init__(self): - super(EmptySequenceSGR, self).__init__(*[]) - - def __add__(self, other: AbstractSequenceSGR) -> AbstractSequenceSGR: - return other - - def print(self) -> str: - return '' - - -class SequenceSGR(AbstractSequenceSGR): - def __init__(self, *params: int): - if len(params) == 0: - raise ValueError('Instantiate EmptySequenceSGR to create no-op SGR') - super(SequenceSGR, self).__init__(*params) - - def print(self) -> str: - return f'{self.CONTROL_CHARACTER}{self.INTRODUCER}' \ - f'{self.SEPARATOR.join([str(param) for param in self._params])}' \ - f'{self.TERMINATOR}' - - -def build(*args: str|int|AbstractSequenceSGR) -> AbstractSequenceSGR: +def build(*args: str | int | SequenceSGR) -> SequenceSGR: result: List[int] = [] for arg in args: @@ -108,17 +93,12 @@ def build(*args: str|int|AbstractSequenceSGR) -> AbstractSequenceSGR: elif isinstance(arg, int): result.append(arg) - elif isinstance(arg, EmptySequenceSGR): - continue - elif isinstance(arg, SequenceSGR): result.extend(arg.params) else: raise TypeError(f'Invalid argument type: {arg!r})') - if len(result) == 0: - return EmptySequenceSGR() return SequenceSGR(*result) diff --git a/tests/test_fmt.py b/tests/test_fmt.py index b0420a0e..2e170ab9 100644 --- a/tests/test_fmt.py +++ b/tests/test_fmt.py @@ -1,7 +1,7 @@ import unittest from pytermor import autof, seq, code -from pytermor.seq import EmptySequenceSGR, SequenceSGR +from pytermor.seq import SequenceSGR class FormatEqualityTestCase(unittest.TestCase): @@ -26,13 +26,13 @@ def test_autof_multiple_sgr(self): self.assertEqual(f.closing_seq, SequenceSGR(code.UNDERLINED_OFF, code.COLOR_OFF, code.BG_COLOR_OFF)) def test_autof_empty_sgr(self): - f = autof(EmptySequenceSGR()) + f = autof(SequenceSGR()) - self.assertEqual(f.opening_seq, EmptySequenceSGR()) - self.assertEqual(f.closing_seq, EmptySequenceSGR()) + self.assertEqual(f.opening_seq, SequenceSGR()) + self.assertEqual(f.closing_seq, SequenceSGR()) def test_autof_multiple_with_empty_sgr(self): - f = autof(seq.BOLD, EmptySequenceSGR(), seq.RED) + f = autof(seq.BOLD, SequenceSGR(), seq.RED) self.assertEqual(f.opening_seq, SequenceSGR(code.BOLD, code.RED)) self.assertEqual(f.closing_seq, SequenceSGR(code.BOLD_DIM_OFF, code.COLOR_OFF)) diff --git a/tests/test_seq.py b/tests/test_seq.py index 486d76a7..003be933 100644 --- a/tests/test_seq.py +++ b/tests/test_seq.py @@ -1,25 +1,27 @@ import unittest -from pytermor import seq, code, build, build_c256 -from pytermor.seq import EmptySequenceSGR, SequenceSGR +from pytermor import seq, code, build, build_c256, build_rgb +from pytermor.seq import SequenceSGR class SequenceEqualityTestCase(unittest.TestCase): def test_regular_is_equal_to_regular(self): - self.assertEqual(SequenceSGR(1, 31, 42), - SequenceSGR(1, 31, 42)) + self.assertEqual(SequenceSGR(1, 31, 42), SequenceSGR(1, 31, 42)) def test_empty_is_equal_to_empty(self): - self.assertEqual(EmptySequenceSGR(), - EmptySequenceSGR()) + self.assertEqual(SequenceSGR(), SequenceSGR()) def test_regular_is_not_equal_to_regular(self): - self.assertNotEqual(SequenceSGR(2, 31, 42), - SequenceSGR(1, 31, 42)) + self.assertNotEqual(SequenceSGR(2, 31, 42), SequenceSGR(1, 31, 42)) def test_empty_is_not_equal_to_regular(self): - self.assertNotEqual(EmptySequenceSGR(), - SequenceSGR(1)) + self.assertNotEqual(SequenceSGR(), SequenceSGR(1)) + + def test_empty_is_not_equal_to_reset(self): + self.assertNotEqual(SequenceSGR(), SequenceSGR(0)) + + def test_reset_is_not_equal_to_empty(self): + self.assertNotEqual(SequenceSGR(), SequenceSGR(0)) class SequenceAdditionTestCase(unittest.TestCase): @@ -27,13 +29,19 @@ def test_addition_of_regular_to_regular(self): self.assertEqual(SequenceSGR(1) + SequenceSGR(3), SequenceSGR(1, 3)) def test_addition_of_regular_to_empty(self): - self.assertEqual(SequenceSGR(41) + EmptySequenceSGR(), SequenceSGR(41)) + self.assertEqual(SequenceSGR(41) + SequenceSGR(), SequenceSGR(41)) def test_addition_of_empty_to_regular(self): - self.assertEqual(EmptySequenceSGR() + SequenceSGR(44), SequenceSGR(44)) + self.assertEqual(SequenceSGR() + SequenceSGR(44), SequenceSGR(44)) def test_addition_of_empty_to_empty(self): - self.assertEqual(EmptySequenceSGR() + EmptySequenceSGR(), EmptySequenceSGR()) + self.assertEqual(SequenceSGR() + SequenceSGR(), SequenceSGR()) + + def test_addition_of_empty_to_reset(self): + self.assertEqual(SequenceSGR() + SequenceSGR(0), SequenceSGR(0)) + + def test_addition_of_reset_to_empty(self): + self.assertEqual(SequenceSGR(0) + SequenceSGR(), SequenceSGR(0)) class SequenceBuildTestCase(unittest.TestCase): # @todo negative @@ -45,6 +53,9 @@ def test_build_key_args(self): s = build('underlined', 'blue', 'bg_black') self.assertEqual(s, SequenceSGR(code.UNDERLINED, code.BLUE, code.BG_BLACK)) + def test_build_key_args_invalid(self): + self.assertRaises(KeyError, build, 'invalid') + def test_build_sgr_args(self): s = build(seq.HI_CYAN, seq.ITALIC) self.assertEqual(s, SequenceSGR(code.HI_CYAN, code.ITALIC)) @@ -53,12 +64,15 @@ def test_build_mixed_args(self): s = build(102, 'bold', seq.INVERSED) self.assertEqual(s, SequenceSGR(code.BG_HI_GREEN, code.BOLD, code.INVERSED)) + def test_build_mixed_args_invalid(self): + self.assertRaises(KeyError, build, 1, 'red', '') + def test_build_empty_arg(self): - s = build(EmptySequenceSGR()) - self.assertEqual(s, EmptySequenceSGR()) + s = build(SequenceSGR()) + self.assertEqual(s, SequenceSGR()) def test_build_mixed_with_empty_arg(self): - s = build(3, EmptySequenceSGR()) + s = build(3, SequenceSGR()) self.assertEqual(s, SequenceSGR(code.ITALIC)) def test_build_c256_foreground(self): @@ -72,5 +86,15 @@ def test_build_c256_background(self): def test_build_c256_invalid(self): self.assertRaises(ValueError, build_c256, 266, bg=True) - # @todo rgb + def test_build_rgb_foreground(self): + s = build_rgb(10, 20, 30) + self.assertEqual(s, SequenceSGR(code.COLOR_EXTENDED, code.EXTENDED_MODE_RGB, 10, 20, 30)) + + def test_build_rgb_background(self): + s = build_rgb(50, 70, 90, bg=True) + self.assertEqual(s, SequenceSGR(code.BG_COLOR_EXTENDED, code.EXTENDED_MODE_RGB, 50, 70, 90)) + def test_build_rgb_invalid(self): + self.assertRaises(ValueError, build_rgb, 10, 310, 30) + self.assertRaises(ValueError, build_rgb, 310, 10, 130) + self.assertRaises(ValueError, build_rgb, 0, 0, 256, bg=True)