Skip to content

Commit

Permalink
Removed excessive _EmptySequenceSGR_: default _SGR_ class without par…
Browse files Browse the repository at this point in the history
…ams was specifically implemented to print out as empty string instead of `\e[m`.
  • Loading branch information
delameter committed Apr 16, 2022
1 parent 510a9db commit 20dfcb5
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 84 deletions.
2 changes: 1 addition & 1 deletion .env.dist
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION=1.4.0
VERSION=1.5.0
PYPI_USERNAME=__token__
PYPI_PASSWORD= #api token
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <u>underlined</u>. 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 <u>underlined</u>. 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:

Expand Down Expand Up @@ -143,7 +143,7 @@ As you can see, the update went well &mdash; 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).

Expand All @@ -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` &mdash; **hard reset** sequence.

### build_c256

Expand Down Expand Up @@ -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_ &mdash; 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_.
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 11 additions & 11 deletions src/pytermor/fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/pytermor/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pytermor import build

from . import code
from .seq import SequenceSGR, AbstractSequenceSGR
from .seq import SequenceSGR


class Registry:
Expand All @@ -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):
Expand Down
60 changes: 20 additions & 40 deletions src/pytermor/seq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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)


Expand Down
10 changes: 5 additions & 5 deletions tests/test_fmt.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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))
Loading

0 comments on commit 20dfcb5

Please sign in to comment.