chromatic-python 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
chromatic/__init__.py CHANGED
@@ -7,6 +7,7 @@ from . import ascii, color, data
7
7
  from .ascii import (
8
8
  AnsiImage,
9
9
  ansi2img,
10
+ ansify,
10
11
  ansi_quantize,
11
12
  ascii2img,
12
13
  ascii_printable,
@@ -40,3 +41,69 @@ from .color import (
40
41
  from .data import register_user_font
41
42
 
42
43
  __all__ = []
44
+
45
+ try:
46
+ import os
47
+ import sys
48
+ from functools import lru_cache, wraps
49
+ from types import ModuleType
50
+
51
+ def find_modules(path: str):
52
+ from setuptools import find_packages
53
+ from pkgutil import iter_modules
54
+
55
+ tree: dict[str, dict | ModuleType] = {__name__: {'__module__': sys.modules[__name__]}}
56
+ children = set()
57
+ for pkg in find_packages(path):
58
+ children.add(pkg)
59
+ pkg_path = path + '/' + pkg.replace('.', '/')
60
+ if sys.version_info.major == 2 or (
61
+ sys.version_info.major == 3 and sys.version_info.minor < 6
62
+ ):
63
+ for _, name, ispkg in iter_modules([pkg_path]):
64
+ if not ispkg:
65
+ children.add(f"{pkg}.{name}")
66
+ else:
67
+ for info in iter_modules([pkg_path]):
68
+ if not info.ispkg:
69
+ children.add(f"{pkg}.{info.name}")
70
+ for child in children:
71
+ name = f"{__name__}.{child}"
72
+ depth = tree
73
+ for node in name.split('.'):
74
+ if node not in depth:
75
+ depth[node] = {}
76
+ depth = depth[node]
77
+ if name in sys.modules:
78
+ depth['__module__'] = sys.modules[name]
79
+ return tree
80
+
81
+ def publicize_modules(modulename: str, tree: dict[str, dict | ModuleType]):
82
+ def is_local(obj: object):
83
+ if isinstance(obj, ModuleType):
84
+ return obj.__spec__.parent == modulename
85
+ elif hasattr(obj, '__module__'):
86
+ return obj.__module__ == modulename
87
+ return obj is not None
88
+
89
+ for name, subtree in tree.items():
90
+ if name == '__module__' and isinstance(subtree, ModuleType):
91
+ submodule = subtree
92
+ init_dir = dir(submodule)
93
+
94
+ @wraps(submodule.__dir__)
95
+ def wrapped():
96
+ s = set(attr for attr in init_dir if is_local(getattr(submodule, attr, None)))
97
+ if hasattr(submodule, '__all__'):
98
+ s.update(submodule.__all__)
99
+ return list(s)
100
+
101
+ sys.modules[submodule.__name__].__dir__ = wrapped
102
+
103
+ else:
104
+ publicize_modules(f"{modulename}.{name}", subtree)
105
+
106
+ publicize_modules(*find_modules(os.path.split(__file__)[0]).popitem())
107
+
108
+ finally:
109
+ del find_modules, publicize_modules
chromatic/_typing.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import operator as op
4
5
  import re
5
6
  import types
6
7
  from collections import OrderedDict, namedtuple
7
8
  from collections.abc import Callable as ABC_Callable
8
9
  from functools import reduce, wraps
9
10
  from numbers import Number
10
- from operator import attrgetter, or_ as bitwise_or
11
11
  from typing import (
12
12
  Any,
13
13
  Callable,
@@ -79,7 +79,7 @@ FontArgType: TypeAlias = FreeTypeFont | UserFont | str | int
79
79
 
80
80
  def type_error_msg(err_obj, *expected, context: str = '', obj_repr=False):
81
81
  n_expected = len(expected)
82
- name_slots = [f"{{{n}.__qualname__!r}}" for n in range(n_expected)]
82
+ name_slots = ['{%d.__qualname__!r}' % n for n in range(n_expected)]
83
83
  if name_slots and n_expected > 1:
84
84
  name_slots[-1] = f"or {name_slots[-1]}"
85
85
  names = (', ' if n_expected > 2 else ' ').join([context.strip()] + name_slots).format(*expected)
@@ -183,7 +183,7 @@ class SupportsUnion(Protocol[_T_contra, _T_co]):
183
183
 
184
184
 
185
185
  def unionize(__iterable: Iterable[SupportsUnion[_T_contra, _T_co]]) -> _T_co:
186
- return reduce(bitwise_or, __iterable)
186
+ return reduce(op.or_, __iterable)
187
187
 
188
188
 
189
189
  _GenericAlias = type(Type[...]) | types.GenericAlias
@@ -213,7 +213,7 @@ class _BoundedDict[_KT, _VT](OrderedDict[_KT, _VT]):
213
213
 
214
214
 
215
215
  _SUBTYPE_CACHE: _BoundedDict[int, ...] = _BoundedDict()
216
- _ATTR_GETTERS: _BoundedDict[..., tuple[Callable[[Iterable], NamedTuple], attrgetter]] = (
216
+ _ATTR_GETTERS: _BoundedDict[..., tuple[Callable[[Iterable], NamedTuple], op.attrgetter]] = (
217
217
  _BoundedDict()
218
218
  )
219
219
 
@@ -247,7 +247,10 @@ def _unique_attrs(obj) -> Optional['NamedTuple']:
247
247
  + 'Attrs'
248
248
  ).strip('_')
249
249
  UniqueAttrs = cast(NamedTuple, namedtuple(tup_name, field_names))
250
- _ATTR_GETTERS[tp] = constructor, getter = UniqueAttrs._make, attrgetter(*attr_names) # noqa
250
+ _ATTR_GETTERS[tp] = [constructor, getter] = [
251
+ UniqueAttrs._make,
252
+ op.attrgetter(*attr_names), # noqa
253
+ ]
251
254
  return constructor(getter(obj))
252
255
 
253
256
 
chromatic/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.1'
21
- __version_tuple__ = version_tuple = (0, 2, 1)
20
+ __version__ = version = '0.2.2'
21
+ __version_tuple__ = version_tuple = (0, 2, 2)
chromatic/ascii/_array.py CHANGED
@@ -1,5 +1,29 @@
1
1
  from __future__ import annotations
2
2
 
3
+ __all__ = [
4
+ 'ansi2img',
5
+ 'ansify',
6
+ 'ansi_quantize',
7
+ 'ascii2img',
8
+ 'contrast_stretch',
9
+ 'equalize_white_point',
10
+ 'get_font_key',
11
+ 'get_font_object',
12
+ 'img2ansi',
13
+ 'img2ascii',
14
+ 'read_ans',
15
+ 'render_ans',
16
+ 'render_font_char',
17
+ 'render_font_str',
18
+ 'reshape_ansi',
19
+ 'scale_saturation',
20
+ 'shuffle_char_set',
21
+ '_scaled_hu_moments',
22
+ 'to_sgr_array',
23
+ 'AnsiImage',
24
+ '_otsu_mask',
25
+ ]
26
+
3
27
  import math
4
28
  import os.path
5
29
  import random
@@ -62,29 +86,6 @@ from ..data import UserFont
62
86
  if TYPE_CHECKING:
63
87
  from _typeshed import AnyStr_co
64
88
 
65
- __all__ = [
66
- 'ansi2img',
67
- 'ansi_quantize',
68
- 'ascii2img',
69
- 'contrast_stretch',
70
- 'equalize_white_point',
71
- 'get_font_key',
72
- 'get_font_object',
73
- 'img2ansi',
74
- 'img2ascii',
75
- 'read_ans',
76
- 'render_ans',
77
- 'render_font_char',
78
- 'render_font_str',
79
- 'reshape_ansi',
80
- 'scale_saturation',
81
- 'shuffle_char_set',
82
- '_scaled_hu_moments',
83
- 'to_sgr_array',
84
- 'AnsiImage',
85
- '_otsu_mask',
86
- ]
87
-
88
89
 
89
90
  def get_font_key(font: FreeTypeFont):
90
91
  """Obtain a unique tuple pair from a FreeTypeFont object.
@@ -597,6 +598,7 @@ def img2ascii(
597
598
  return ascii_str
598
599
 
599
600
 
601
+ @rgb_dispatch
600
602
  def img2ansi(
601
603
  __img: RGBImageLike | PathLike[str] | str,
602
604
  __font: FontArgType = 'arial.ttf',
@@ -605,7 +607,7 @@ def img2ansi(
605
607
  ansi_type: AnsiColorParam = DEFAULT_ANSI,
606
608
  sort_glyphs: bool | type[reversed] = True,
607
609
  equalize: bool | Literal['white_point'] = True,
608
- bg: Color | Int3Tuple = (0, 0, 0),
610
+ bg: Color | Int3Tuple | str = (0, 0, 0),
609
611
  ):
610
612
  """Convert an image to an ANSI array.
611
613
 
@@ -665,7 +667,7 @@ def img2ansi(
665
667
  """
666
668
  if ansi_type is not DEFAULT_ANSI:
667
669
  ansi_type = get_ansi_type(ansi_type)
668
- bg_wrapper = ColorStr('{}', color_spec={'bg': bg}, ansi_type=ansi_type, no_reset=True)
670
+ bg_wrapper = ColorStr('{}', color_spec={'bg': bg}, ansi_type=ansi_type, reset=False)
669
671
  base_ascii, color_arr = img2ascii(__img, __font, factor, char_set, sort_glyphs, ret_img=True)
670
672
  lines = base_ascii.splitlines()
671
673
  h, w = map(len, (lines, lines[0]))
@@ -697,7 +699,7 @@ def ascii2img(
697
699
  font_size=24,
698
700
  *,
699
701
  fg: Int3Tuple | str = (0, 0, 0),
700
- bg: Int3Tuple | str = (255, 255, 255),
702
+ bg: Int3Tuple | str = (0xFF, 0xFF, 0xFF),
701
703
  ):
702
704
  """Render a literal string as an image.
703
705
 
@@ -803,7 +805,7 @@ def ansi2img(
803
805
  x_offset = 0
804
806
  for color_str in row:
805
807
  text_width = font.getbbox(color_str.base_str)[2]
806
- if color_str._sgr_.is_reset():
808
+ if getattr(color_str, '_sgr_').is_reset():
807
809
  fg_default = None
808
810
  bg_default = input_bg
809
811
  if fg_color := getattr(color_str.fg, 'rgb', fg_default):
@@ -823,6 +825,38 @@ def ansi2img(
823
825
  return img
824
826
 
825
827
 
828
+ def ansify(
829
+ __img: RGBImageLike | PathLike[str] | str,
830
+ /,
831
+ *,
832
+ font: FontArgType = UserFont.IBM_VGA_437_8X16,
833
+ font_size: int = 16,
834
+ factor: int = 200,
835
+ char_set: Iterable[str] = None,
836
+ sort_glyphs: bool | type[reversed] = True,
837
+ ansi_type: AnsiColorParam = DEFAULT_ANSI,
838
+ equalize: bool | Literal['white_point'] = True,
839
+ fg: Int3Tuple | str = (170, 170, 170),
840
+ bg: Int3Tuple | str | Literal['auto'] = (0, 0, 0),
841
+ ):
842
+ return ansi2img(
843
+ img2ansi(
844
+ __img,
845
+ font,
846
+ factor=factor,
847
+ char_set=char_set,
848
+ ansi_type=ansi_type,
849
+ sort_glyphs=sort_glyphs,
850
+ equalize=equalize,
851
+ bg=bg,
852
+ ),
853
+ font,
854
+ font_size=font_size,
855
+ fg_default=fg,
856
+ bg_default=bg,
857
+ )
858
+
859
+
826
860
  def _is_array(__obj: Any) -> TypeGuard[ndarray]:
827
861
  return isinstance(__obj, ndarray)
828
862
 
@@ -913,6 +947,7 @@ def to_sgr_array(__str: str, ansi_type: AnsiColorParam = DEFAULT_ANSI):
913
947
  resets = frozenset(resets_btok)
914
948
  pad_esc = {0x1B: '\x00\x1b'}
915
949
  x = []
950
+ sgr: SgrSequence | None
916
951
  for line in __str.splitlines():
917
952
  xs = []
918
953
  ansi_meta = {}
@@ -927,7 +962,7 @@ def to_sgr_array(__str: str, ansi_type: AnsiColorParam = DEFAULT_ANSI):
927
962
  else:
928
963
  cs = new_cs(s)
929
964
  if sgr is None:
930
- sgr = cs._sgr_
965
+ sgr: SgrSequence = getattr(cs, '_sgr_')
931
966
  if sgr.is_reset():
932
967
  ansi_meta.clear()
933
968
  for k in resets.intersection(sgr.values()):
@@ -947,7 +982,7 @@ def to_sgr_array(__str: str, ansi_type: AnsiColorParam = DEFAULT_ANSI):
947
982
  if not ansi_meta.get('bright'):
948
983
  ansi_meta['bright'] = True
949
984
  new_sgr = SgrSequence(
950
- sgr.values() + [p._value_ for p in prev._sgr_ if p.is_color()]
985
+ sgr.values() + [p._value_ for p in getattr(prev, '_sgr_') if p.is_color()]
951
986
  )
952
987
  prev = cs = new_cs(cs.base_str, new_sgr)
953
988
  xs.append(cs.as_ansi_type(ansi_typ))
@@ -1009,9 +1044,10 @@ def read_ans[AnyStr: (str, bytes)](__path: PathLike[AnyStr] | AnyStr, encoding='
1009
1044
  class AnsiImage:
1010
1045
 
1011
1046
  @classmethod
1012
- def open[
1013
- AnyStr: (str, bytes)
1014
- ](
1047
+ def open[AnyStr: (
1048
+ str,
1049
+ bytes,
1050
+ )](
1015
1051
  cls,
1016
1052
  fp: PathLike[AnyStr] | AnyStr,
1017
1053
  shape: TupleOf2[int],
@@ -1076,7 +1112,7 @@ class AnsiImage:
1076
1112
  if not __table:
1077
1113
  return self
1078
1114
  table = {
1079
- k: (v if v not in frozenset(x for c in ' \t\n\r\v\f' for x in (c, ord(c))) else ' ')
1115
+ k: v if v not in frozenset(x for c in ' \t\n\r\v\f' for x in (c, ord(c))) else ' '
1080
1116
  for (k, v) in __table.items()
1081
1117
  if k != ord('\n')
1082
1118
  }
@@ -1098,8 +1134,8 @@ class AnsiImage:
1098
1134
  return getattr(self, attr_name)[0]
1099
1135
  lines = []
1100
1136
  for line in self.data:
1101
- buffer = []
1102
1137
  if line:
1138
+ buffer = []
1103
1139
  initial = line[0]
1104
1140
  for s in line[1:]:
1105
1141
  if s.ansi_partition()[::2] == initial.ansi_partition()[::2]:
@@ -1109,7 +1145,7 @@ class AnsiImage:
1109
1145
  initial = s
1110
1146
  buffer.append(initial)
1111
1147
  lines.append(''.join(buffer))
1112
- setattr(self, attr_name, ('\n'.join(lines) + SGR_RESET, self.shape))
1148
+ setattr(self, attr_name, ('\n'.join(lines) + SGR_RESET.decode(), self.shape))
1113
1149
  return getattr(self, attr_name)[0]
1114
1150
 
1115
1151
 
chromatic/color/core.py CHANGED
@@ -61,7 +61,8 @@ from .._typing import (
61
61
  os.system('')
62
62
 
63
63
  CSI: Final[bytes] = b'['
64
- SGR_RESET: Final[str] = ''
64
+ SGR_RESET: Final[bytes] = b''
65
+ SGR_RESET_S: Final[str] = SGR_RESET.decode()
65
66
 
66
67
  # ansi color global lookups
67
68
  # ansi 4bit {color code (int) ==> (key, RGB)}
@@ -503,7 +504,8 @@ def get_ansi_type(typ):
503
504
 
504
505
  def rgb2ansi_escape(ret_format, mode, rgb):
505
506
  ret_format = get_ansi_type(ret_format)
506
- assert len(rgb) == 3, 'length of RGB value is not 3'
507
+ if len(rgb) != 3:
508
+ raise ValueError('length of RGB value is not 3')
507
509
  try:
508
510
  if ret_format is ansicolor4Bit:
509
511
  return b'%d' % _ANSI16C_KV2I[mode, nearest_ansi_4bit_rgb(rgb)]
@@ -633,9 +635,11 @@ def _get_sgr_bitmask[_T: (bytes, bytearray, Buffer)](__x: _T) -> list[int]:
633
635
  return allocated
634
636
 
635
637
 
636
- def _iter_normalized_sgr[
637
- _T: (Buffer, SgrParamWrapper, int)
638
- ](__iter: Buffer | Iterable[_T]) -> Iterator[AnsiColorFormat | int]:
638
+ def _iter_normalized_sgr[_T: (
639
+ Buffer,
640
+ SgrParamWrapper,
641
+ int,
642
+ )](__iter: Buffer | Iterable[_T]) -> Iterator[AnsiColorFormat | int]:
639
643
  if isinstance(__iter, Buffer):
640
644
  yield from _get_sgr_bitmask(__iter)
641
645
  else:
@@ -894,7 +898,8 @@ class SgrSequence(Sequence[SgrParamWrapper]):
894
898
  if ansi_type is None:
895
899
  is_diff_ansi_typ = lambda _: False
896
900
  else:
897
- assert ansi_type in _ANSI_COLOR_TYPES
901
+ if ansi_type not in _ANSI_COLOR_TYPES:
902
+ raise TypeError
898
903
  is_diff_ansi_typ = lambda v: type(v) is not ansi_type
899
904
 
900
905
  for x in _iter_sgr(__iter):
@@ -1013,15 +1018,17 @@ _VALID_KEYS: frozenset[str] = unionize(
1013
1018
  )
1014
1019
 
1015
1020
 
1016
- def _solve_color_spec[
1017
- _T: (_CSpecType, SgrSequence)
1018
- ](color_spec: Optional[_T], ansi_type: AnsiColorType):
1021
+ def _solve_color_spec[_ColorSpec: (
1022
+ _CSpecType,
1023
+ SgrSequence,
1024
+ )](color_spec: Optional[_ColorSpec], ansi_type: AnsiColorType):
1019
1025
  keys: list[str] = ['bg', 'fg']
1020
1026
 
1021
1027
  def resolve(value, *, key=None):
1022
1028
  nonlocal keys
1023
1029
  if key is not None:
1024
- assert key in _VALID_KEYS, 'expected literal keys {}, got {!r}'.format(_VALID_KEYS, key)
1030
+ if key not in _VALID_KEYS:
1031
+ raise ValueError(f"expected one of {_VALID_KEYS}, got {key!r}")
1025
1032
  if key in keys:
1026
1033
  keys.remove(key)
1027
1034
  match value:
@@ -1031,7 +1038,8 @@ def _solve_color_spec[
1031
1038
  r, g, b = (x & 0xFF for x in rgb)
1032
1039
  yield (key or keys.pop(), (r, g, b))
1033
1040
  case np.ndarray() as colors:
1034
- assert not colors.shape[-1] % 3, 'array does not contain RGB values'
1041
+ if colors.shape[-1] % 3 != 0:
1042
+ raise ValueError('array does not contain RGB values')
1035
1043
  it = np.uint8(colors).flat
1036
1044
  for _ in range(colors.ndim):
1037
1045
  yield (key or keys.pop(), tuple(int(next(it)) for _ in range(3)))
@@ -1076,7 +1084,7 @@ def _get_color_str_vars(
1076
1084
  if csi_count := color_spec.count(CSI):
1077
1085
  if csi_count > 1:
1078
1086
  color_spec, _, byte_str = (
1079
- color_spec.removeprefix(CSI).removesuffix(SGR_RESET.encode()).partition(b'm')
1087
+ color_spec.removeprefix(CSI).removesuffix(SGR_RESET).partition(b'm')
1080
1088
  )
1081
1089
  if color_spec.count(CSI) > 1:
1082
1090
  raise ValueError(
@@ -1095,26 +1103,12 @@ def _get_color_str_vars(
1095
1103
  return sgr_params, base_str
1096
1104
 
1097
1105
 
1098
- class _ColorStrWeakVars(TypedDict, total=False):
1099
- _base_str_: str
1100
- _sgr_: SgrSequence
1101
- _no_reset_: bool
1102
-
1103
-
1104
1106
  class _AnsiBytesGetter:
1105
1107
 
1106
1108
  def __get__(self, instance: Optional['ColorStr'], objtype=None):
1107
1109
  if instance is None:
1108
1110
  return
1109
- return instance._sgr_.__bytes__()
1110
-
1111
-
1112
- class _SgrParamsGetter:
1113
-
1114
- def __get__(self, instance: Optional['ColorStr'], objtype=None):
1115
- if instance is None:
1116
- return
1117
- return instance._sgr_._sgr_params_
1111
+ return bytes(getattr(instance, '_sgr_'))
1118
1112
 
1119
1113
 
1120
1114
  class _ColorDictGetter:
@@ -1122,7 +1116,7 @@ class _ColorDictGetter:
1122
1116
  def __get__(self, instance: Optional['ColorStr'], objtype=None):
1123
1117
  if instance is None:
1124
1118
  return
1125
- return {k: Color.from_rgb(v) for k, v in instance._sgr_.rgb_dict.items()}
1119
+ return {k: Color.from_rgb(v) for k, v in getattr(instance, '_sgr_').rgb_dict.items()}
1126
1120
 
1127
1121
 
1128
1122
  class ColorStr(str):
@@ -1130,16 +1124,16 @@ class ColorStr(str):
1130
1124
  def _weak_var_update(self, **kwargs):
1131
1125
  if kwargs.keys().isdisjoint(inst_vars := vars(self)):
1132
1126
  raise ValueError(f"unexpected keys: {kwargs.keys() - inst_vars.keys()}") from None
1133
- sgr = kwargs.get('_sgr_', self._sgr_)
1134
- base_str = kwargs.get('_base_str_', self._base_str_)
1135
- suffix = '' if kwargs.get('_no_reset_', self._no_reset_) else SGR_RESET
1127
+ sgr = kwargs.get('_sgr_', getattr(self, '_sgr_'))
1128
+ base_str = kwargs.get('_base_str_', self.base_str)
1129
+ suffix = SGR_RESET_S if kwargs.get('_reset_', self.reset) else ''
1136
1130
  inst = super().__new__(ColorStr, ''.join([str(sgr), base_str, suffix]))
1137
- inst.__dict__ |= {**inst_vars, **kwargs}
1131
+ inst.__dict__ |= inst_vars | kwargs
1138
1132
  return cast(ColorStr, inst)
1139
1133
 
1140
1134
  def ansi_partition(self):
1141
1135
  """Returns the 3-tuple: SGR sequence prefix, base string, SGR reset (or empty string)."""
1142
- return str(self._sgr_), self.base_str, '' if self.no_reset else SGR_RESET
1136
+ return str(getattr(self, '_sgr_')), self.base_str, SGR_RESET_S if self.reset else ''
1143
1137
 
1144
1138
  def as_ansi_type(self, __ansi_type):
1145
1139
  """Convert all ANSI color codes in the :class:`ColorStr` to a single ANSI type.
@@ -1157,24 +1151,24 @@ class ColorStr(str):
1157
1151
 
1158
1152
  """
1159
1153
  ansi_type = get_ansi_type(__ansi_type)
1160
- if self._sgr_.is_color():
1154
+ if getattr(self, '_sgr_').is_color():
1161
1155
  new_params = []
1162
1156
  new_rgb = {}
1163
- for p in self._sgr_params_:
1157
+ for p in getattr(self, '_sgr_'):
1164
1158
  if p.is_color() and type(p._value_) is not ansi_type:
1165
1159
  new_ansi = ansi_type(p._value_)
1166
1160
  new_rgb |= new_ansi.rgb_dict
1167
1161
  new_params.append(SgrParamWrapper(new_ansi))
1168
1162
  else:
1169
1163
  new_params.append(p)
1170
- if new_params == self._sgr_params_:
1164
+ if new_params == list(getattr(self, '_sgr_')):
1171
1165
  return self
1172
1166
  new_sgr = SgrSequence()
1173
1167
  for name, value in zip(('_sgr_params_', '_rgb_dict_'), (new_params, new_rgb)):
1174
1168
  setattr(new_sgr, name, value)
1175
1169
  inst = super().__new__(
1176
1170
  type(self),
1177
- ''.join([str(new_sgr), self._base_str_, '' if self._no_reset_ else SGR_RESET]),
1171
+ ''.join([str(new_sgr), self.base_str, SGR_RESET_S if self.reset else '']),
1178
1172
  )
1179
1173
  for name, value in {**vars(self), '_sgr_': new_sgr, '_ansi_type_': ansi_type}.items():
1180
1174
  setattr(inst, name, value)
@@ -1241,7 +1235,7 @@ class ColorStr(str):
1241
1235
  """
1242
1236
  if __value:
1243
1237
  if isinstance(__value, ColorStr):
1244
- kwargs = __value._color_dict_
1238
+ kwargs = getattr(__value, '_color_dict_')
1245
1239
  else:
1246
1240
  raise TypeError(
1247
1241
  f"expected positional argument of type {ColorStr.__qualname__!r}, "
@@ -1255,7 +1249,7 @@ class ColorStr(str):
1255
1249
  kwargs = {k: v if v is None else Color(v) for k, v in kwargs.items()}
1256
1250
  except Exception as e:
1257
1251
  raise e from None
1258
- sgr = SgrSequence(self._sgr_)
1252
+ sgr = SgrSequence(getattr(self, '_sgr_'))
1259
1253
  if bool(absolute):
1260
1254
  del sgr.rgb_dict
1261
1255
  sgr.rgb_dict = (self.ansi_format, kwargs)
@@ -1297,7 +1291,7 @@ class ColorStr(str):
1297
1291
  Examples
1298
1292
  --------
1299
1293
  >>> # creating an empty ColorStr object
1300
- >>> empty_cs = ColorStr(no_reset=True)
1294
+ >>> empty_cs = ColorStr(reset=True)
1301
1295
  >>> empty_cs.ansi
1302
1296
  b''
1303
1297
 
@@ -1327,7 +1321,7 @@ class ColorStr(str):
1327
1321
  return self
1328
1322
  if any(not isinstance(x, int) or x in _ANSI256_KEY2I.values() for x in p):
1329
1323
  raise ValueError
1330
- new_sgr = SgrSequence(self._sgr_)
1324
+ new_sgr = SgrSequence(getattr(self, '_sgr_'))
1331
1325
  for x in p:
1332
1326
  if x in new_sgr:
1333
1327
  new_sgr.pop(new_sgr.index(x))
@@ -1345,8 +1339,7 @@ class ColorStr(str):
1345
1339
  else:
1346
1340
  ansi_type = self.ansi_format
1347
1341
  inst = super().__new__(
1348
- type(self),
1349
- ''.join([str(new_sgr), self._base_str_, '' if self._no_reset_ else SGR_RESET]),
1342
+ type(self), ''.join([str(new_sgr), self.base_str, SGR_RESET_S if self.reset else ''])
1350
1343
  )
1351
1344
  inst.__dict__ |= {**vars(self), '_sgr_': new_sgr, '_ansi_type_': ansi_type}
1352
1345
  return cast(ColorStr, inst)
@@ -1354,11 +1347,11 @@ class ColorStr(str):
1354
1347
  def __add__(self, other):
1355
1348
  if type(self) is type(other):
1356
1349
  return self._weak_var_update(
1357
- _sgr_=self._sgr_ + other._sgr_,
1358
- _base_str_=''.join([self._base_str_, other._base_str_]),
1350
+ _sgr_=getattr(self, '_sgr_') + other._sgr_,
1351
+ _base_str_=''.join([self.base_str, other._base_str_]),
1359
1352
  )
1360
1353
  if isinstance(other, str):
1361
- return self._weak_var_update(_base_str_=''.join([self._base_str_, other]))
1354
+ return self._weak_var_update(_base_str_=''.join([self.base_str, other]))
1362
1355
  if isinstance(other, SgrParameter):
1363
1356
  return self.update_sgr(other)
1364
1357
  if hasattr(other, '_sgr_'):
@@ -1373,10 +1366,10 @@ class ColorStr(str):
1373
1366
  def __contains__(self, __key: str):
1374
1367
  if type(__key) is not str:
1375
1368
  return False
1376
- if __key == str(self._sgr_):
1369
+ if __key == str(getattr(self, '_sgr_')):
1377
1370
  return True
1378
- if __key == SGR_RESET:
1379
- return not self.no_reset
1371
+ if __key == SGR_RESET_S:
1372
+ return self.reset
1380
1373
  return self.base_str.__contains__(__key)
1381
1374
 
1382
1375
  def __eq__(self, other):
@@ -1395,7 +1388,7 @@ class ColorStr(str):
1395
1388
  return self._weak_var_update(_base_str_=self.base_str[__key])
1396
1389
 
1397
1390
  def __hash__(self):
1398
- return str(self).__hash__()
1391
+ return ''.join(self.ansi_partition()).__hash__()
1399
1392
 
1400
1393
  # noinspection PyUnusedLocal
1401
1394
  def __init__(self, obj=None, color_spec=None, **kwargs):
@@ -1429,9 +1422,9 @@ class ColorStr(str):
1429
1422
  * ANSI format can also be changed on instances using :meth:`ColorStr.as_ansi_type`
1430
1423
  * Reformatting recursively applies to `alt_spec` if `alt_spec` is not None
1431
1424
 
1432
- no_reset : bool
1433
- If True, create the :class:`ColorStr` without concatenating a 'reset all' SGR sequence.
1434
- Default is False (new instances get concatenated with reset sequences).
1425
+ reset : bool
1426
+ If False, create the :class:`ColorStr` without concatenating an SGR 'reset' sequence.
1427
+ Default is True (new instances get concatenated with reset sequences).
1435
1428
 
1436
1429
  Returns
1437
1430
  -------
@@ -1479,10 +1472,10 @@ class ColorStr(str):
1479
1472
  def __matmul__(self, other):
1480
1473
  """Return a new :class:`ColorStr` with the base string of `self` and colors of `other`"""
1481
1474
  if type(self) is type(other):
1482
- return self._weak_var_update(_sgr_=other._sgr_, _no_reset_=other.no_reset)
1475
+ return self._weak_var_update(_sgr_=getattr(other, '_sgr_'), _reset_=other.reset)
1483
1476
  raise TypeError(
1484
1477
  'unsupported operand type(s) for @: '
1485
- "{!r} and {!r}".format(*(type(x).__qualname__ for x in (self, other)))
1478
+ "{.__qualname__!r} and {.__qualname__!r}".format(*map(type, (self, other)))
1486
1479
  )
1487
1480
 
1488
1481
  def __mod__(self, __value):
@@ -1493,8 +1486,8 @@ class ColorStr(str):
1493
1486
 
1494
1487
  def __invert__(self):
1495
1488
  """Return a copy of `self` with inverted colors (XORed by '0xFFFFFF')"""
1496
- sgr = SgrSequence(self._sgr_)
1497
- sgr.rgb_dict = (self.ansi_format, {k: ~v for k, v in self._color_dict_.items()})
1489
+ sgr = SgrSequence(getattr(self, '_sgr_'))
1490
+ sgr.rgb_dict = (self.ansi_format, {k: ~v for k, v in getattr(self, '_color_dict_').items()})
1498
1491
  return self._weak_var_update(_sgr_=sgr)
1499
1492
 
1500
1493
  def __new__(cls, obj=None, color_spec=None, **kwargs):
@@ -1507,9 +1500,11 @@ class ColorStr(str):
1507
1500
  for name, value in vars(color_spec).items():
1508
1501
  setattr(inst, name, value)
1509
1502
  return inst
1510
- d = {'_ansi_type_': ansi_type or DEFAULT_ANSI}
1511
- no_reset = d['_no_reset_'] = bool(kwargs.get('no_reset', False))
1512
- suffix = '' if no_reset else SGR_RESET
1503
+ d = {
1504
+ '_ansi_type_': ansi_type or DEFAULT_ANSI,
1505
+ '_reset_': bool(kwargs.get('reset', True)),
1506
+ }
1507
+ suffix = SGR_RESET_S if d['_reset_'] else ''
1513
1508
  if obj is not None:
1514
1509
  if not isinstance(obj, str):
1515
1510
  obj = str(obj, encoding='ansi') if isinstance(obj, Buffer) else str(obj)
@@ -1559,48 +1554,47 @@ class ColorStr(str):
1559
1554
  }
1560
1555
  if not diff_dict:
1561
1556
  return self
1562
- sgr = SgrSequence(self._sgr_)
1557
+ sgr = SgrSequence(getattr(self, '_sgr_'))
1563
1558
  sgr.rgb_dict = self.ansi_format, diff_dict
1564
1559
  return self._weak_var_update(_sgr_=sgr)
1565
1560
 
1566
1561
  _ansi_ = _AnsiBytesGetter()
1567
1562
  _color_dict_ = _ColorDictGetter()
1568
- _sgr_params_ = _SgrParamsGetter()
1569
1563
 
1570
1564
  @property
1571
1565
  def ansi(self):
1572
- return self._ansi_
1566
+ return getattr(self, '_ansi_')
1573
1567
 
1574
1568
  @property
1575
1569
  def ansi_format(self):
1576
- return self._ansi_type_
1570
+ return getattr(self, '_ansi_type_')
1577
1571
 
1578
1572
  @property
1579
1573
  def base_str(self):
1580
1574
  """The non-ANSI part of the string"""
1581
- return self._base_str_
1575
+ return getattr(self, '_base_str_')
1582
1576
 
1583
1577
  @property
1584
1578
  def bg(self):
1585
1579
  """Background color"""
1586
- return self._color_dict_.get('bg')
1580
+ return getattr(self, '_color_dict_').get('bg')
1587
1581
 
1588
1582
  @property
1589
1583
  def fg(self):
1590
1584
  """Foreground color"""
1591
- return self._color_dict_.get('fg')
1585
+ return getattr(self, '_color_dict_').get('fg')
1592
1586
 
1593
1587
  @property
1594
- def no_reset(self):
1595
- return self._no_reset_
1588
+ def reset(self):
1589
+ return getattr(self, '_reset_')
1596
1590
 
1597
1591
  @property
1598
1592
  def rgb_dict(self):
1599
- return {k: v.rgb for k, v in self._color_dict_.items()}
1593
+ return {k: v.rgb for k, v in getattr(self, '_color_dict_').items()}
1600
1594
 
1601
1595
 
1602
1596
  def _color_str_to_mask(cs: ColorStr) -> tuple[SgrSequence, str]:
1603
- return cs._sgr_, cs.base_str
1597
+ return getattr(cs, '_sgr_'), cs.base_str
1604
1598
 
1605
1599
 
1606
1600
  class color_chain:
@@ -1728,7 +1722,7 @@ class color_chain:
1728
1722
 
1729
1723
  def __str__(self):
1730
1724
  return ''.join(
1731
- str(ColorStr(base_str, color_spec=sgr, ansi_type=self._ansi_type_, no_reset=True))
1725
+ str(ColorStr(base_str, color_spec=sgr, ansi_type=self._ansi_type_, reset=False))
1732
1726
  for sgr, base_str in self.masks
1733
1727
  )
1734
1728
 
chromatic/color/core.pyi CHANGED
@@ -99,7 +99,7 @@ class ColorStr(str):
99
99
  def __hash__(self) -> int: ...
100
100
  def __init__(
101
101
  self,
102
- obj: object = None,
102
+ obj: object = ...,
103
103
  color_spec: Union[_ColorSpec, ColorStr] = None,
104
104
  **kwargs: Unpack[_ColorStrKwargs],
105
105
  ) -> None: ...
@@ -111,7 +111,7 @@ class ColorStr(str):
111
111
  def __invert__(self) -> ColorStr: ...
112
112
  def __new__(
113
113
  cls,
114
- obj: object = None,
114
+ obj: object = ...,
115
115
  color_spec: Union[_ColorSpec, ColorStr] = None,
116
116
  **kwargs: Unpack[_ColorStrKwargs],
117
117
  ) -> ColorStr: ...
@@ -136,7 +136,7 @@ class ColorStr(str):
136
136
  @property
137
137
  def fg(self) -> Optional[Color]: ...
138
138
  @property
139
- def no_reset(self) -> bool: ...
139
+ def reset(self) -> bool: ...
140
140
  @property
141
141
  def rgb_dict(self) -> dict[ColorDictKeys, Int3Tuple]: ...
142
142
 
@@ -277,9 +277,13 @@ class SgrSequence(Sequence[SgrParamWrapper]):
277
277
  def __getitem__(self, item: SupportsIndex) -> SgrParamWrapper: ...
278
278
  @overload
279
279
  def __getitem__(self, item: slice) -> list[SgrParamWrapper]: ...
280
- def __init__[
281
- _T: (SgrParamWrapper, Buffer, int), _AnsiType: type[AnsiColorFormat]
282
- ](self, __iter: Iterable[_T] = None, *, ansi_type: _AnsiType = None) -> None: ...
280
+ def __init__[_T: (
281
+ SgrParamWrapper,
282
+ Buffer,
283
+ int,
284
+ ), _AnsiType: type[AnsiColorFormat]](
285
+ self, __iter: Iterable[_T] = None, *, ansi_type: _AnsiType = None
286
+ ) -> None: ...
283
287
  def __iter__(self) -> Iterator[SgrParamWrapper]: ...
284
288
  def __len__(self) -> int: ...
285
289
  def __radd__[_T: (SgrSequence, str)](self, other: _T) -> _T: ...
@@ -307,9 +311,9 @@ class SgrSequence(Sequence[SgrParamWrapper]):
307
311
 
308
312
  # noinspection PyUnresolvedReferences
309
313
  @rgb_dict.setter
310
- def rgb_dict[
311
- _AnsiColorType: type[AnsiColorFormat]
312
- ](self, __value: tuple[_AnsiColorType, dict[ColorDictKeys, Optional[Color]]]) -> None: ...
314
+ def rgb_dict[_AnsiColorType: type[AnsiColorFormat]](
315
+ self, __value: tuple[_AnsiColorType, dict[ColorDictKeys, Optional[Color]]]
316
+ ) -> None: ...
313
317
 
314
318
  class _ColorDict(TypedDict, total=False):
315
319
  bg: Optional[Color | AnsiColorFormat]
@@ -317,15 +321,16 @@ class _ColorDict(TypedDict, total=False):
317
321
 
318
322
  class _ColorStrKwargs(TypedDict, total=False):
319
323
  ansi_type: Optional[AnsiColorAlias | type[AnsiColorFormat]]
320
- no_reset: bool
324
+ reset: bool
321
325
 
322
326
  class _ColorStrWeakVars(TypedDict, total=False):
323
327
  _base_str_: str
324
- _no_reset_: bool
328
+ _reset_: bool
325
329
  _sgr_: SgrSequence
326
330
 
327
- CSI: Final[bytes] = b'['
328
- SGR_RESET: Final[str] = ''
331
+ CSI: Final[bytes]
332
+ SGR_RESET: Final[bytes]
333
+ SGR_RESET_S: Final[str]
329
334
  DEFAULT_ANSI: Final[type[ansicolor8Bit | ansicolor4Bit]]
330
335
 
331
336
  _ANSI16C_BRIGHT: Final[frozenset[int]]
@@ -1,4 +1,4 @@
1
- from functools import lru_cache, update_wrapper
1
+ from functools import lru_cache, update_wrapper, wraps
2
2
  from inspect import getfullargspec, getmodule, isbuiltin, signature
3
3
  from types import FunctionType, MappingProxyType
4
4
  from typing import Callable, Iterator, Sequence, TYPE_CHECKING, Union, cast, dataclass_transform
@@ -109,16 +109,18 @@ def _check_if_ns_member(cls: type) -> Callable[[str], bool]:
109
109
  return lambda x: member_type == anno_dict.get(x)
110
110
 
111
111
 
112
- def _ns_from_iter[
113
- _KT, _VT
114
- ](__iter: Iterator[_KT] | Callable[[], Iterator[_KT]], member_type: _VT = null) -> Callable[
115
- [type[DynamicNamespace[_VT]]], type[DynamicNamespace[_VT]]
116
- ]:
112
+ def _ns_from_iter[_KT, _VT](
113
+ __iter: Iterator[_KT] | Callable[[], Iterator[_KT]], member_type: type[_VT] = object
114
+ ) -> Callable[[type[DynamicNamespace[_VT]]], type[DynamicNamespace[_VT]]]:
117
115
  def decorator(cls: type[DynamicNamespace[_VT]]):
118
116
  anno = cls.__annotations__
119
117
  type_params = cls.__type_params__
120
118
  m_iter = __iter() if callable(__iter) else iter(__iter)
121
- members: Iterator[_KT] = m_iter if member_type == null else map(member_type, m_iter)
119
+ members: Iterator[_KT] = (
120
+ m_iter
121
+ if (member_type is object or not isinstance(member_type, type))
122
+ else map(member_type, m_iter)
123
+ )
122
124
  d = dict(zip((k for k, v in anno.items() if v in type_params), members))
123
125
  cls.__init__ = update_wrapper(
124
126
  lambda *args, **kwargs: cls.__base__.__init__(*args, **(kwargs | d)), cls.__init__
@@ -430,73 +432,73 @@ class _color_ns_getter:
430
432
  ) from None
431
433
 
432
434
 
433
- _dummy_func = lambda *args, **kwargs: ...
434
- _dummy_signature = signature(_dummy_func)
435
-
436
-
437
- class rgb_dispatch[**P, R]: # noqa
438
- color_ns = cast(MappingProxyType[str, Int3Tuple], _color_ns_getter())
439
-
440
- __signature__ = _dummy_signature
435
+ def rgb_dispatch[**P, R](
436
+ __f: Callable[P, R] = None, /, *, var_names: Sequence[str] = ()
437
+ ) -> Callable[P, R]:
438
+ dummy_func = lambda *args, **kwargs: ...
439
+ [rgb_args, variadic] = [set[str]() for _ in range(2)]
441
440
 
442
- def __init__(self, __f: Callable[P, R] = None, /, *, args: Sequence[str] = ()):
443
- if isinstance(args, str):
444
- args = (args,)
441
+ def fix_signature():
442
+ nonlocal var_names, rgb_args, variadic
443
+ if isinstance(var_names, str):
444
+ var_names = (var_names,)
445
445
  if not callable(__f):
446
446
  raise ValueError
447
- self._func = __f
448
447
  try:
449
- argspec = getfullargspec(self._func)
450
- sig = signature(self._func)
448
+ argspec = getfullargspec(__f)
449
+ sig = signature(__f)
451
450
  except TypeError:
452
- if not (isbuiltin(self._func) or getattr(self._func, '__module__', '') == 'builtins'):
451
+ if not (isbuiltin(__f) or getattr(__f, '__module__', '') == 'builtins'):
453
452
  raise
454
- argspec = getfullargspec(_dummy_func)
455
- sig = self.__signature__
456
- self._variadic = {argspec.varargs, argspec.varkw}
457
- self._variadic.discard(None)
458
- all_args = self._variadic.union(argspec.args + argspec.kwonlyargs)
459
- self._rgb_args = all_args.intersection(
460
- {'*': argspec.varargs, '**': argspec.varkw}.get(arg) or arg for arg in args
453
+ argspec = getfullargspec(dummy_func)
454
+ sig = signature(dummy_func)
455
+ variadic = {argspec.varargs, argspec.varkw}
456
+ variadic.discard(None)
457
+ all_args = variadic.union(argspec.args + argspec.kwonlyargs)
458
+ rgb_args = all_args.intersection(
459
+ {'*': argspec.varargs, '**': argspec.varkw}.get(arg) or arg for arg in var_names
461
460
  )
462
- if not self._rgb_args:
461
+ if not rgb_args:
463
462
  keys = frozenset({'fg', 'bg'})
464
463
  for arg in all_args:
465
464
  if (arg[:2] in keys) or (arg[-2:] in keys):
466
- self._rgb_args.add(arg)
467
- self._variadic &= self._rgb_args
465
+ rgb_args.add(arg)
466
+ variadic &= rgb_args
468
467
  parameters = []
469
468
  for name, param in sig.parameters.items():
470
- if name not in self._rgb_args or param.annotation is param.empty:
469
+ if name not in rgb_args or param.annotation is param.empty:
471
470
  parameters.append(param)
472
471
  else:
473
472
  anno = param.annotation
474
473
  parameters.append(
475
474
  param.replace(
476
475
  annotation=str
477
- | eval(getattr(anno, '__name__', str(anno)), getmodule(self._func).__dict__)
476
+ | eval(getattr(anno, '__name__', str(anno)), getmodule(__f).__dict__)
478
477
  )
479
478
  )
480
- self.__signature__ = sig.replace(parameters=parameters)
481
- update_wrapper(self, self._func)
479
+ return sig.replace(parameters=parameters)
480
+
481
+ __f.__signature__ = fix_signature()
482
+ color_ns = cast(MappingProxyType[str, Int3Tuple], _color_ns_getter())
482
483
 
483
- def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
484
- bound = self.__signature__.bind(*args, **kwargs)
484
+ @wraps(__f)
485
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
486
+ bound = getattr(__f, '__signature__').bind(*args, **kwargs)
485
487
  bound.apply_defaults()
486
488
  for arg, value in bound.arguments.items():
487
- if arg not in self._rgb_args:
489
+ if arg not in rgb_args:
488
490
  continue
489
- if arg in self._variadic:
491
+ if arg in variadic:
490
492
  bound.arguments[arg] = (
491
- tuple(self.color_ns[v] if v in self.color_ns else v for v in value)
493
+ tuple(color_ns[v] if v in color_ns else v for v in value)
492
494
  if isinstance(value, tuple)
493
- else {
494
- k: self.color_ns[v] if v in self.color_ns else v for k, v in value.items()
495
- }
495
+ else {k: color_ns[v] if v in color_ns else v for k, v in value.items()}
496
496
  )
497
- elif value in self.color_ns:
498
- bound.arguments[arg] = self.color_ns[value]
499
- return self._func(*bound.args, **bound.kwargs)
497
+ elif value in color_ns:
498
+ bound.arguments[arg] = color_ns[value]
499
+ return __f(*bound.args, **bound.kwargs)
500
+
501
+ return wrapper
500
502
 
501
503
 
502
504
  # fmt: off
@@ -1,9 +1,8 @@
1
1
  __all__ = ['Back', 'ColorNamespace', 'Fore', 'Style', 'rgb_dispatch', 'named_color']
2
2
 
3
- import inspect
4
3
  from collections.abc import Sequence
5
- from types import MappingProxyType
6
- from typing import Callable, Iterator, Literal, TypeAlias, TypeVar, Union, overload
4
+ from types import FunctionType
5
+ from typing import Callable, Iterator, Literal, TypeAlias, TypeVar, Union
7
6
 
8
7
  from .core import Color, ColorStr, color_chain
9
8
  from .._typing import Int3Tuple
@@ -298,17 +297,7 @@ named_color: Union[
298
297
  dict[Literal['24b'], Callable[[_ColorName], Color]],
299
298
  ]
300
299
 
301
- class rgb_dispatch[**P, R]:
302
- color_ns: MappingProxyType[str, Int3Tuple]
303
-
304
- __signature__: inspect.Signature
305
-
306
- @overload
307
- def __new__(cls, __f: Callable[P, R], /, *, args: Sequence[str] = ()):
308
- return __f
309
-
310
- @property
311
- def __wrapped__(self) -> Callable[P, R]: ...
300
+ def rgb_dispatch[F: (type, FunctionType)](__f: F, /, *, var_names: Sequence[str] = ()) -> F: ...
312
301
 
313
302
  Back = AnsiBack()
314
303
  Fore = AnsiFore()
@@ -36,12 +36,8 @@ def _build_config():
36
36
  f: SupportsWrite
37
37
  d: dict[Literal['fonts', 'images'] | Literal['__hash__'], dict[str, str] | str]
38
38
  d = {'fonts': {}, 'images': {}, '__hash__': ''}
39
- printable = ''.join(
40
- c for c in printable if any((c.isalnum(), c.isidentifier(), c == '.'))
41
- )
42
- for font_fp in (
43
- (Path(fonts) / x).absolute() for x in filter(_is_ttf_ext, os.listdir(fonts))
44
- ):
39
+ printable = ''.join(c for c in printable if any((c.isalnum(), c.isidentifier(), c == '.')))
40
+ for font_fp in ((Path(fonts) / x).absolute() for x in filter(_is_ttf_ext, os.listdir(fonts))):
45
41
  font_name = font_fp.stem
46
42
  for c in set(font_name):
47
43
  if c not in printable:
@@ -71,9 +67,7 @@ def _validate(**kwargs):
71
67
  s1, s2 = (
72
68
  set(
73
69
  str(fp.relative_to(data_root))
74
- for fp in (
75
- (Path(subdir) / x).absolute() for x in filter(f, os.listdir(subdir))
76
- )
70
+ for fp in ((Path(subdir) / x).absolute() for x in filter(f, os.listdir(subdir)))
77
71
  )
78
72
  for f, subdir in zip((_is_ttf_ext, _is_img_ext), (fonts, images))
79
73
  )
@@ -119,9 +113,7 @@ def _create_font_enum() -> type['UserFont']:
119
113
  def path(self):
120
114
  return data_root / Path(_font_data_[self.name])
121
115
 
122
- enum_cls = IntEnum(
123
- 'UserFont', {k: i for (i, k) in enumerate(sorted(_font_data_.keys()))}
124
- )
116
+ enum_cls = IntEnum('UserFont', {k: i for (i, k) in enumerate(sorted(_font_data_.keys()))})
125
117
  enum_cls.path = property(path)
126
118
  return enum_cls
127
119
 
@@ -156,9 +148,7 @@ def register_user_font[AnyStr: (str, bytes)](__path: AnyStr | os.PathLike[AnyStr
156
148
  if path_obj.is_symlink():
157
149
  path_obj = path_obj.readlink()
158
150
  if path_obj.suffix != '.ttf':
159
- raise ValueError(
160
- f"Expected '.ttf' file, " f"got filetype {path_obj.suffix!r} instead"
161
- )
151
+ raise ValueError(f"Expected '.ttf' file, " f"got filetype {path_obj.suffix!r} instead")
162
152
  from PIL.ImageFont import FreeTypeFont
163
153
 
164
154
  try:
chromatic/demo.py CHANGED
@@ -143,7 +143,7 @@ def named_colors():
143
143
  )
144
144
  buffer.append(xs)
145
145
  for ln in buffer:
146
- print(' | '.join(ln))
146
+ print(' | '.join(map(str, ln)))
147
147
 
148
148
 
149
149
  def color_table():
@@ -174,12 +174,12 @@ def color_table():
174
174
  for c in colors
175
175
  ]
176
176
  bg_colors = [ColorStr().recolor(bg=None)] + [c.recolor(fg=None, bg=c.fg) for c in fg_colors]
177
- pad = spacing - 1
178
- print('|'.join([f"{'4bit': ^{pad}}", f"{'8bit': ^{pad}}", f"{'24bit': >{pad}}"]))
177
+ print('|'.join(f"{'%dbit' % n: {'>' if n > 9 else '^'}{spacing - 1}}" for n in (4, 8, 24)))
178
+ suffix = '\x1b[0m' if sys.stdout.isatty() else ''
179
179
  for row in fg_colors:
180
180
  for col in bg_colors:
181
181
  for typ in ansi_types:
182
- print(row.as_ansi_type(typ).recolor(bg=col.bg), end='\x1b[0m')
182
+ print(row.as_ansi_type(typ).recolor(bg=col.bg), end=suffix)
183
183
  print()
184
184
  print('\nstyles:')
185
185
  print()
@@ -195,7 +195,7 @@ def color_table():
195
195
  for style in style_params:
196
196
  print(
197
197
  ColorStr('.'.join([SgrParameter.__qualname__, style.name])).update_sgr(style),
198
- end='\x1b[0m' + (' ' * 4),
198
+ end=suffix + (' ' * 4),
199
199
  )
200
200
  print()
201
201
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chromatic-python
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: ANSI art image processing and colored terminal text
5
5
  Author: crypt0lith
6
6
  License: MIT License
@@ -27,20 +27,21 @@ License: MIT License
27
27
  Project-URL: Homepage, https://github.com/crypt0lith/chromatic
28
28
  Keywords: ansi,ascii,art,font,image,terminal,parser
29
29
  Classifier: Programming Language :: Python :: 3.12
30
+ Classifier: License :: OSI Approved :: MIT License
30
31
  Classifier: Operating System :: OS Independent
31
32
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
32
33
  Classifier: Typing :: Typed
33
34
  Requires-Python: >=3.12
34
35
  Description-Content-Type: text/markdown
35
36
  License-File: LICENSE
36
- Requires-Dist: fonttools~=4.51.0
37
37
  Requires-Dist: networkx~=3.4.2
38
- Requires-Dist: numpy~=2.1.1
39
- Requires-Dist: opencv-python~=4.10.0.84
40
- Requires-Dist: pillow~=10.4.0
41
- Requires-Dist: scikit-image~=0.25.0rc1
42
- Requires-Dist: scikit-learn~=1.5.2
43
- Requires-Dist: scipy~=1.14.1
38
+ Requires-Dist: numpy~=2.2.6
39
+ Requires-Dist: pillow~=11.2.1
40
+ Requires-Dist: opencv-python~=4.11.0.86
41
+ Requires-Dist: scikit-image~=0.25.2
42
+ Requires-Dist: scikit-learn~=1.6.1
43
+ Requires-Dist: fonttools~=4.58.0
44
+ Requires-Dist: scipy~=1.15.3
44
45
  Dynamic: license-file
45
46
 
46
47
  ![image](/logo/logo.PNG)
@@ -1,19 +1,19 @@
1
- chromatic/__init__.py,sha256=kDmPuhakyiB85eBKsRcu67p1XD--9ZY4uiWI_3QfvPs,792
2
- chromatic/_typing.py,sha256=vcdS3dYwZPhpA6wWg3MDSiO2OhyaToRjfHYoBez3R5Q,14711
3
- chromatic/_version.py,sha256=cTPlZaUCc20I4ZWsDjY35UftpFNRgfDaDBgkWxfIQmg,532
4
- chromatic/demo.py,sha256=gz2wpXZv01YVF-xF8EA3YK6LnCPSXiYGKtTwmVB7SKQ,12698
1
+ chromatic/__init__.py,sha256=qDsFcpBycwOTojKMKlpyfkj0pj3wUFDswZtxP2EDSzg,3309
2
+ chromatic/_typing.py,sha256=aPdYI3nFpkGArErzZzv73Rf-Bur3WOsaRyfy8CbWRtE,14714
3
+ chromatic/_version.py,sha256=UC90NvqLUDpj_FE8q0wCmEOBTFqTc2wo52DnLF1e7lU,532
4
+ chromatic/demo.py,sha256=WmtHsJ96MvepLwXBIo8XVcalgAw5P5nlb_TZLRN45qE,12744
5
5
  chromatic/ascii/__init__.py,sha256=Ic3bqfqr0uAJd6vB4kToDookvZVr8Nrkyy3speiH5oM,164
6
- chromatic/ascii/_array.py,sha256=P7PINIQJ7ZAJKKQbgn8i7lgm8NEOK9W89MAb81dZE-s,38286
6
+ chromatic/ascii/_array.py,sha256=ZLNU1x1Jabz9b9Tky6eKKLQyM5gt3tBrks7eegIbtu8,39270
7
7
  chromatic/ascii/_curses.py,sha256=Mz-_FgFUvACjMZyFoy1LXpxJzg5ekcyjMV8fYHXHcpA,3632
8
8
  chromatic/ascii/_glyph_proc.py,sha256=VVRMFRu4oT7iH-q1MMgmZAnuQ0hbSrmtqtt9q3JnOLQ,2865
9
9
  chromatic/color/__init__.py,sha256=8dnpWqbmEciFgJrQpUC0gV9M5D1bKIF7d9Sl4mj0x7g,185
10
10
  chromatic/color/colorconv.py,sha256=2FygfpgNX6pfo47DhXNqwA46Nnpw8TbmCD1ThuIdZn4,8400
11
- chromatic/color/core.py,sha256=mynDpSLMvz93UW_OjMyj3wKU_4wsc5zC_kfPsa4q7MQ,59657
12
- chromatic/color/core.pyi,sha256=E1EMxhR7g7ICdOypRptBefE8Cx2Wslo6-xYeTPtM0no,12011
11
+ chromatic/color/core.py,sha256=vTAHdUY4oZBAgQIz0vaisapasLJab9lVE1emr6FWF8c,59732
12
+ chromatic/color/core.pyi,sha256=nQnfzmA7bPY9rSr7EC9tXrJ_R4NST9VD_6yqZTBxixU,12045
13
13
  chromatic/color/iterators.py,sha256=v-JirP7ZvcDzsccD5Cctwsnu3qndCjOolGliAqN0azU,6099
14
- chromatic/color/palette.py,sha256=WuJLfQJPSGkzs7708AS-cybtUzYzdMYWwZM9eM5PmsI,18817
15
- chromatic/color/palette.pyi,sha256=1_4rpw4rGSbZjdEwSMVDeu6bK6Y5ym0_2-gCYwM7p8w,10481
16
- chromatic/data/__init__.py,sha256=IVfKnpn5iKSk9GYIm3uhdE5810dVla36H0bboeYXTcw,6650
14
+ chromatic/color/palette.py,sha256=Fz_mC7IUeI3-YF23aJPpF5RKVLOsQgWe17Lfq5uGplo,18802
15
+ chromatic/color/palette.pyi,sha256=Umdp_Ichm3-avs-bkwv3Nwb9lyquFOdEtlAhwuqrMGE,10252
16
+ chromatic/data/__init__.py,sha256=6-nmWy2dSlXblkwbtAmsg55HXmVnAGBSOJYb0j7j098,6538
17
17
  chromatic/data/__init__.pyi,sha256=AZGa46n1Hck3xHN91Mmm7Qibg--CYweKJMMFPHH6yY8,556
18
18
  chromatic/data/fonts/IBM_VGA_437_8x16.ttf,sha256=qMdn-pJWJNKNmHnDoDqGIE94vOTezaCiBv0VK92QbJQ,50124
19
19
  chromatic/data/fonts/consolas.ttf,sha256=xubOgRn91H7GpUSaCOLSrX9B6gMUOq4ZMGjtn6WOrrw,459180
@@ -21,8 +21,8 @@ chromatic/data/images/butterfly.jpg,sha256=rnSfMVotSgpTNRPQ9CzYpDp8S8lkCAJ5ZySRr
21
21
  chromatic/data/images/escher.png,sha256=FvAC-WOkUVIqHBxasfgCJbH1CN1IwLE-JyMiATOEKFc,132667
22
22
  chromatic/data/images/goblin_virus.png,sha256=ygs19t4ZeZYsbiRFXjRyEz8OtgC9Zyl8owX0O-5n40o,8849
23
23
  chromatic/data/images/hotdog.jpg,sha256=b0vnoQCarhq-gFu3df512qPTvauvF1Oa0oXVO9msEFc,60823
24
- chromatic_python-0.2.1.dist-info/licenses/LICENSE,sha256=e7GjzmO7L12JDai3XRWHD8PgZvFmzNceEztB6d-9kuA,1086
25
- chromatic_python-0.2.1.dist-info/METADATA,sha256=Ao57WdhTMs-9FEqHHJHcErYQJP_jxlvd0aG9hijCAaA,5993
26
- chromatic_python-0.2.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
27
- chromatic_python-0.2.1.dist-info/top_level.txt,sha256=wjzxcxfjO8I4u22BS8wL67Bq64c59kbZCqQ--Fc_Mqw,10
28
- chromatic_python-0.2.1.dist-info/RECORD,,
24
+ chromatic_python-0.2.2.dist-info/licenses/LICENSE,sha256=e7GjzmO7L12JDai3XRWHD8PgZvFmzNceEztB6d-9kuA,1086
25
+ chromatic_python-0.2.2.dist-info/METADATA,sha256=2AVOeLAWh5W6zhoSx8d6_KHqPFdc3hypl_8Kjzbcg8M,6042
26
+ chromatic_python-0.2.2.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
27
+ chromatic_python-0.2.2.dist-info/top_level.txt,sha256=wjzxcxfjO8I4u22BS8wL67Bq64c59kbZCqQ--Fc_Mqw,10
28
+ chromatic_python-0.2.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5