chromatic-python 0.1.0__py3-none-any.whl → 0.2.0__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
@@ -1,5 +1,11 @@
1
+ try:
2
+ from ._version import version as __version__
3
+ except ImportError:
4
+ __version__ = "0.0.0"
5
+
1
6
  from . import ascii, color, data
2
7
  from .ascii import (
8
+ AnsiImage,
3
9
  ansi2img,
4
10
  ansi_quantize,
5
11
  ascii2img,
@@ -14,12 +20,22 @@ from .ascii import (
14
20
  read_ans,
15
21
  render_ans,
16
22
  reshape_ansi,
17
- to_sgr_array
23
+ to_sgr_array,
18
24
  )
19
25
  from .ascii._glyph_proc import get_glyph_masks
20
26
  from .color import (
21
- ansicolor24Bit, ansicolor4Bit, ansicolor8Bit, Back, Color, colorbytes, ColorStr, Fore,
22
- SgrParameter, Style
27
+ Back,
28
+ Color,
29
+ ColorNamespace,
30
+ ColorStr,
31
+ Fore,
32
+ SgrParameter,
33
+ Style,
34
+ ansicolor24Bit,
35
+ ansicolor4Bit,
36
+ ansicolor8Bit,
37
+ colorbytes,
38
+ named_color,
23
39
  )
24
40
  from .data import register_user_font
25
41
 
chromatic/_typing.py CHANGED
@@ -1,18 +1,44 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Mapping
4
- from functools import reduce
3
+ import inspect
4
+ import re
5
+ import types
6
+ from collections import OrderedDict, namedtuple
7
+ from collections.abc import Callable as ABC_Callable
8
+ from functools import reduce, wraps
5
9
  from numbers import Number
6
- from types import UnionType
10
+ from operator import attrgetter, or_ as bitwise_or
7
11
  from typing import (
8
- Any, Callable, Concatenate, get_args, get_origin, get_type_hints, Iterable, Literal, ParamSpec,
9
- Protocol, Sequence, SupportsRound, TYPE_CHECKING, TypeVar, Union
12
+ Any,
13
+ Callable,
14
+ Concatenate,
15
+ Hashable,
16
+ Iterable,
17
+ Literal,
18
+ NamedTuple,
19
+ Optional,
20
+ ParamSpec,
21
+ Protocol,
22
+ Sequence,
23
+ Type,
24
+ TypeAlias,
25
+ TypeAliasType,
26
+ TypeGuard,
27
+ TypeVar,
28
+ TypedDict,
29
+ Union,
30
+ Unpack,
31
+ cast,
32
+ get_args,
33
+ get_origin,
34
+ get_type_hints,
35
+ runtime_checkable,
10
36
  )
11
37
 
12
- from numpy import dtype, float64, generic, ndarray, number, uint8
13
- from numpy._typing import _ArrayLike, NDArray
14
38
  from PIL.Image import Image
15
39
  from PIL.ImageFont import FreeTypeFont
40
+ from numpy import dtype, float64, generic, ndarray, number, uint8
41
+ from numpy._typing import NDArray, _ArrayLike
16
42
 
17
43
  from chromatic.data import UserFont
18
44
 
@@ -22,44 +48,50 @@ _T_co = TypeVar('_T_co', covariant=True)
22
48
  _T_contra = TypeVar('_T_contra', contravariant=True)
23
49
  _AnyNumber_co = TypeVar('_AnyNumber_co', number, Number, covariant=True)
24
50
 
25
- if TYPE_CHECKING:
26
- from _typeshed import SupportsRichComparison, SupportsDivMod
27
-
28
-
29
- class SupportsRoundAndDivMod(
30
- SupportsRound[_T_co],
31
- SupportsDivMod[Any, _T_co],
32
- Protocol
33
- ):
34
- ...
35
-
36
51
  type ArrayReducerFunc[_SCT: generic] = Callable[Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]]
37
- type KeyFunction[_T] = Callable[[_T], SupportsRichComparison]
38
52
  type ShapedNDArray[_Shape: tuple[int, ...], _SCT: generic] = ndarray[_Shape, dtype[_SCT]]
39
53
  type MatrixLike[_SCT: generic] = ShapedNDArray[TupleOf2[int], _SCT]
40
54
  type SquareMatrix[_I: int, _SCT: generic] = ShapedNDArray[TupleOf2[_I], _SCT]
41
55
  type GlyphArray[_SCT: generic] = SquareMatrix[Literal[24], _SCT]
42
56
  type TupleOf2[_T] = tuple[_T, _T]
43
57
  type TupleOf3[_T] = tuple[_T, _T, _T]
44
- Float3Tuple = TupleOf3[float]
45
- Int3Tuple = TupleOf3[int]
46
- FloatSequence = Sequence[float]
47
- IntSequence = Sequence[int]
48
- GlyphBitmask = GlyphArray[bool]
49
- Bitmask = MatrixLike[bool]
50
- GreyscaleGlyphArray = GlyphArray[float64]
51
- GreyscaleArray = MatrixLike[float64]
52
- RGBArray = ShapedNDArray[tuple[int, int, Literal[3]], uint8]
53
- RGBPixel = ShapedNDArray[tuple[Literal[3]], uint8]
54
-
55
- RGBImageLike = Union[Image, RGBArray]
56
- RGBVectorLike = Union[Int3Tuple, IntSequence, RGBPixel]
58
+
59
+ Float3Tuple: TypeAlias = TupleOf3[float]
60
+ Int3Tuple: TypeAlias = TupleOf3[int]
61
+ FloatSequence: TypeAlias = Sequence[float]
62
+ IntSequence: TypeAlias = Sequence[int]
63
+ GlyphBitmask: TypeAlias = GlyphArray[bool]
64
+ Bitmask: TypeAlias = MatrixLike[bool]
65
+ GreyscaleGlyphArray: TypeAlias = GlyphArray[float64]
66
+ GreyscaleArray: TypeAlias = MatrixLike[float64]
67
+ RGBArray: TypeAlias = ShapedNDArray[tuple[int, int, Literal[3]], uint8]
68
+ RGBPixel: TypeAlias = ShapedNDArray[tuple[Literal[3]], uint8]
69
+
70
+ RGBImageLike: TypeAlias = Image | RGBArray
71
+ RGBVectorLike: TypeAlias = Int3Tuple | IntSequence | RGBPixel
57
72
  ColorDictKeys = Literal['fg', 'bg']
58
73
  Ansi4BitAlias = Literal['4b']
59
74
  Ansi8BitAlias = Literal['8b']
60
75
  Ansi24BitAlias = Literal['24b']
61
76
  AnsiColorAlias = Ansi4BitAlias | Ansi8BitAlias | Ansi24BitAlias
62
- FontArgType = Union[FreeTypeFont | UserFont, str, int]
77
+ FontArgType: TypeAlias = FreeTypeFont | UserFont | str | int
78
+
79
+
80
+ def type_error_msg(err_obj, *expected, context: str = '', obj_repr=False):
81
+ n_expected = len(expected)
82
+ name_slots = [f"{{{n}.__qualname__!r}}" for n in range(n_expected)]
83
+ if name_slots and n_expected > 1:
84
+ name_slots[-1] = f"or {name_slots[-1]}"
85
+ names = (', ' if n_expected > 2 else ' ').join([context.strip()] + name_slots).format(*expected)
86
+ if not obj_repr:
87
+ if not isinstance(err_obj, type):
88
+ err_obj = type(err_obj)
89
+ oops = repr(err_obj.__qualname__)
90
+ elif not isinstance(err_obj, str):
91
+ oops = repr(err_obj)
92
+ else:
93
+ oops = err_obj
94
+ return f"expected {names}, got {oops} instead"
63
95
 
64
96
 
65
97
  def is_matching_type(value, typ):
@@ -72,8 +104,7 @@ def is_matching_type(value, typ):
72
104
  return value in args
73
105
  elif isinstance(typ, TypeVar):
74
106
  if typ.__constraints__:
75
- return any(
76
- is_matching_type(value, constraint) for constraint in typ.__constraints__)
107
+ return any(is_matching_type(value, constraint) for constraint in typ.__constraints__)
77
108
  else:
78
109
  return True
79
110
  elif origin is type:
@@ -101,8 +132,9 @@ def is_matching_type(value, typ):
101
132
  return True
102
133
  key_type, val_type = args
103
134
  return all(
104
- is_matching_type(k, key_type) and is_matching_type(v, val_type) for k, v in
105
- value.items())
135
+ is_matching_type(k, key_type) and is_matching_type(v, val_type)
136
+ for k, v in value.items()
137
+ )
106
138
  elif origin is tuple:
107
139
  if not isinstance(value, tuple):
108
140
  return False
@@ -118,51 +150,250 @@ def is_matching_type(value, typ):
118
150
  return False
119
151
 
120
152
 
153
+ def is_matching_typed_dict(__d: dict, typed_dict: type[dict]) -> tuple[bool, str]:
154
+ if TypedDict not in getattr(__d, '__orig_bases__', ()):
155
+ return False, type_error_msg(__d, dict)
156
+ expected = get_type_hints(typed_dict)
157
+ if unexpected := __d.keys() - expected:
158
+ return False, f"unexpected keyword arguments: {unexpected}"
159
+ if missing := set(getattr(typed_dict, '__required_keys__', expected)) - __d.keys():
160
+ return False, f"missing required keys: {missing}"
161
+ for name, typ in expected.items():
162
+ field = __d.get(name)
163
+ if field is None or is_matching_type(field, typ):
164
+ continue
165
+ return False, type_error_msg(field, typ, context=f'keyword argument {name!r} of type')
166
+ return True, ''
167
+
168
+
169
+ def is_matching_callable(value, expected_type):
170
+ return callable(value) and value is expected_type
171
+
172
+
121
173
  def deconstruct_type(tp):
122
174
  origin = get_origin(tp) or tp
123
175
  args = get_args(tp)
124
176
  return origin, args
125
177
 
126
178
 
127
- def is_matching_callable(value, expected_type):
128
- if not callable(value):
129
- return False
130
- return id(value) == id(expected_type)
179
+ @runtime_checkable
180
+ class SupportsUnion(Protocol[_T_contra, _T_co]):
131
181
 
182
+ def __or__(self, x: _T_contra, /) -> _T_co: ...
132
183
 
133
- def pseudo_union(ts: Iterable[type]) -> Union[type, UnionType]:
134
- return reduce(lambda x, y: x | y, ts)
135
184
 
185
+ def unionize(__iterable: Iterable[SupportsUnion[_T_contra, _T_co]]) -> _T_co:
186
+ return reduce(bitwise_or, __iterable)
136
187
 
137
- def type_error_msg(err_obj, *expected, context: str = '', obj_repr=False):
138
- n_expected = len(expected)
139
- name_slots = [f"{{{n}.__qualname__!r}}" for n in range(n_expected)]
140
- if name_slots and n_expected > 1:
141
- name_slots[-1] = f"or {name_slots[-1]}"
142
- names = (', ' if n_expected > 2 else ' ').join(
143
- [context.strip()] + name_slots).format(*expected)
144
- if not obj_repr:
145
- if not isinstance(err_obj, type):
146
- err_obj = type(err_obj)
147
- oops = repr(err_obj.__qualname__)
148
- elif not isinstance(err_obj, str):
149
- oops = repr(err_obj)
150
- else:
151
- oops = err_obj
152
- return f"expected {names}, got {oops} instead"
153
188
 
189
+ _GenericAlias = type(Type[...]) | types.GenericAlias
190
+ _UnionGenericType = type(Union[..., None])
191
+ _LiteralGenericType = type(Literal[''])
192
+ _CallableGenericType = type(Callable[[], ...]) | type(ABC_Callable[[], ...])
193
+ _CallableType = type(Callable) | ABC_Callable
154
194
 
155
- def is_matching_typed_dict(__d: dict, typed_dict: type[dict]) -> tuple[bool, str]:
156
- if not isinstance(__d, dict):
157
- return False, type_error_msg(__d, dict)
158
- expected = get_type_hints(typed_dict)
159
- if unexpected := __d.keys() - expected:
160
- return False, f"unexpected keyword arguments: {unexpected}"
161
- if missing := set(getattr(typed_dict, '__required_keys__', expected)) - __d.keys():
162
- return False, f"missing required keys: {missing}"
163
- for name, typ in expected.items():
164
- if ((field := __d.get(name)) is not None) and not is_matching_type(field, typ):
165
- return False, type_error_msg(
166
- field, typ,
167
- context=f'keyword argument {name!r} of type')
168
- return True, ''
195
+
196
+ class _BoundedDict[_KT, _VT](OrderedDict[_KT, _VT]):
197
+
198
+ def __init__(self, *, maxsize: Optional[int] = 128):
199
+ """Bounded OrderedDict, mimics FIFO behavior of `functools.lru_cache`"""
200
+ super().__init__()
201
+ if maxsize is not None:
202
+ maxsize = max(maxsize, 0)
203
+
204
+ @wraps(self.__setitem__)
205
+ def _fifo(_, key, value):
206
+ if maxsize <= len(self):
207
+ self.popitem(last=True)
208
+ super(_BoundedDict, self).__setitem__(key, value)
209
+
210
+ self.__setitem__ = _fifo
211
+
212
+ __repr__ = dict.__repr__
213
+
214
+
215
+ _SUBTYPE_CACHE: _BoundedDict[int, ...] = _BoundedDict()
216
+ _ATTR_GETTERS: _BoundedDict[..., tuple[Callable[[Iterable], NamedTuple], attrgetter]] = (
217
+ _BoundedDict()
218
+ )
219
+
220
+
221
+ def _unique_attrs(obj) -> Optional['NamedTuple']:
222
+ tp = type(obj)
223
+ tp_name: str = tp.__name__
224
+ if tp is type:
225
+ return
226
+ elif tp in _ATTR_GETTERS:
227
+ constructor, getter = _ATTR_GETTERS[tp]
228
+ return constructor(getter(obj))
229
+ ignored_attrs = frozenset({'__module__', '__slots__'})
230
+ attr_names = sorted(
231
+ s
232
+ for t in tp.mro()[:-1]
233
+ for s in set(dir(t)).difference(ignored_attrs, *map(dir, t.mro()[1:]))
234
+ if not callable(getattr(tp, s, None))
235
+ )
236
+ if '__dict__' in attr_names:
237
+ attr_names = sorted(cast(dict[str, ...], obj.__dict__).keys() - ignored_attrs)
238
+ if not attr_names:
239
+ return
240
+ attr_names, field_names = _sort_attrs(obj, tp_name, attr_names)
241
+ tup_name = (
242
+ re.sub(
243
+ r'\b[a-z0-9_]+\b',
244
+ lambda m: ''.join(s.capitalize() for s in m.group(0).split('_')),
245
+ tp_name.strip(),
246
+ )
247
+ + 'Attrs'
248
+ ).strip('_')
249
+ UniqueAttrs = cast(NamedTuple, namedtuple(tup_name, field_names))
250
+ _ATTR_GETTERS[tp] = constructor, getter = UniqueAttrs._make, attrgetter(*attr_names) # noqa
251
+ return constructor(getter(obj))
252
+
253
+
254
+ def _sort_attrs(obj, tp_name, attr_names):
255
+ field_names = [name.strip('_') for name in attr_names]
256
+ inf = float('inf')
257
+ try:
258
+ sig = inspect.signature(type(obj))
259
+ indices = (
260
+ dict.fromkeys(field_names, inf) | {p: i for i, p in enumerate(sig.parameters)}
261
+ ).values()
262
+ for names in (attr_names, field_names):
263
+ names.sort(key=dict(zip(names, indices)).__getitem__)
264
+ return attr_names, field_names
265
+ except ValueError:
266
+ maybe_sigs: set[str | None] = set()
267
+ text_signature = '__text_signature__'
268
+ if text_signature in attr_names:
269
+ attr_names.remove(text_signature)
270
+ field_names.remove(text_signature.strip('_'))
271
+ maybe_sigs.add(getattr(obj, text_signature, None))
272
+ elif doc := getattr(obj, '__doc__', None):
273
+ lines = iter(inspect.cleandoc(doc).splitlines(keepends=True))
274
+ no_square_parens = {ord(c): None for c in '[]'}
275
+ sig_start = tp_name + '('
276
+ while True:
277
+ try:
278
+ line = next(lines)
279
+ while sig_start not in line:
280
+ line = next(lines)
281
+ _, _, params = line.partition(sig_start)
282
+ params, _, _ = (s.translate(no_square_parens) for s in params.partition(')'))
283
+ maybe_sigs.add(params)
284
+ except StopIteration:
285
+ break
286
+ maybe_sigs.discard(None)
287
+ if maybe_sigs:
288
+ if len(maybe_sigs) > 1:
289
+ sig = max(
290
+ maybe_sigs, key=lambda s: sum(1 for sub in s.split(', ') if sub in field_names)
291
+ )
292
+ else:
293
+ sig = maybe_sigs.pop()
294
+ positions = {x: i for i, x in enumerate(sig.split(', ')) if x}
295
+ sorted_field_names = sorted(field_names, key=lambda k: positions.get(k, inf))
296
+ transitions = {idx: sorted_field_names.index(x) for idx, x in enumerate(field_names)}
297
+ field_names = sorted_field_names
298
+ attr_names = [attr_names[k] for k in map(transitions.__getitem__, range(len(attr_names)))]
299
+ for Names in (attr_names, field_names):
300
+ name_attr = next((s for s in Names if s.strip('_') == 'name'), None)
301
+ if name_attr is not None:
302
+ Names.sort(key=name_attr.__eq__, reverse=True)
303
+ return attr_names, field_names
304
+
305
+
306
+ def subtype[_T](typ: _T) -> _T:
307
+ type TypeVarDict = dict[TypeVar, ...]
308
+
309
+ def _serialize(__item: tuple[Any, Hashable]):
310
+ key, value = __item
311
+ return _unique_attrs(key), hash(value)
312
+
313
+ def _cache_key(tp, tvars: TypeVarDict):
314
+ if tvars:
315
+ value = tp, *sorted(tvars.items(), key=_serialize)
316
+ else:
317
+ value = tp
318
+ return value
319
+
320
+ def _inner(tp: ..., tvars: TypeVarDict):
321
+ key = _cache_key(tp, tvars)
322
+ recursive = lambda x: _inner(x, tvars)
323
+ try:
324
+ if key in _SUBTYPE_CACHE:
325
+ return _SUBTYPE_CACHE[key]
326
+ elif tp in tvars:
327
+ return tvars[tp]
328
+ except TypeError:
329
+ pass
330
+ if isinstance(tp, (_UnionGenericType, types.UnionType)):
331
+ args = tp.__args__
332
+ args_list = list(args)
333
+ if (
334
+ literals := [
335
+ idx for idx, elem in enumerate(args) if isinstance(elem, _LiteralGenericType)
336
+ ]
337
+ ) and len(literals) > 1:
338
+
339
+ def _next_args(__index: int):
340
+ idx = args_list.index(args[__index])
341
+ value = args_list.pop(idx)
342
+ return getattr(value, '__args__', ())
343
+
344
+ start = args[literals.pop(0)]
345
+ args_list[args_list.index(start)] = Literal[
346
+ *start.__args__,
347
+ *dict.fromkeys(arg for idx in literals for arg in _next_args(idx)),
348
+ ]
349
+ try:
350
+ return unionize(map(recursive, args_list))
351
+ except TypeError:
352
+ return Union[*map(recursive, args_list)]
353
+ elif isinstance(tp, TypeAliasType):
354
+ result = recursive(tp.__value__)
355
+ elif isinstance(tp, _CallableGenericType):
356
+ ts, rtype = get_args(tp)
357
+ if isinstance(ts, list):
358
+ ts = list(map(recursive, ts))
359
+ result = ABC_Callable[ts, recursive(rtype)]
360
+ elif isinstance(tp, _GenericAlias):
361
+ origin, args = cast(tuple[types.GenericAlias, tuple], deconstruct_type(tp))
362
+ if origin_params := dict(zip(getattr(origin, '__parameters__', ()), args)):
363
+ if arg_match := origin_params.keys() & tvars:
364
+ _union = unionize(
365
+ origin[*map(f, arg_match)]
366
+ for f in (tvars.__getitem__, origin_params.__getitem__)
367
+ )
368
+ key = _cache_key(_union, {})
369
+ result = _SUBTYPE_CACHE[key] = _inner(_union, {})
370
+ return result
371
+ for param, arg in origin_params.items():
372
+ tvars[param] = recursive(arg)
373
+ if isinstance(origin, TypeAliasType):
374
+ result = recursive(origin)
375
+ elif (
376
+ isinstance(origin, type)
377
+ and issubclass(origin, tuple)
378
+ and len(args) == 2
379
+ and args[-1] is Ellipsis
380
+ ):
381
+ result = origin[recursive(args[0]), ...]
382
+ else:
383
+ try:
384
+ result = origin[*map(recursive, args)]
385
+ except TypeError:
386
+ if origin is Unpack or origin is TypeGuard:
387
+ result = origin[recursive(args[0])]
388
+ else:
389
+ raise
390
+ elif tp is Ellipsis:
391
+ return Any
392
+ elif tp is None or tp is types.NoneType:
393
+ return None
394
+ else:
395
+ return tp
396
+ _SUBTYPE_CACHE[key] = result
397
+ return result
398
+
399
+ return _inner(typ, {})
chromatic/_version.py ADDED
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.2.0'
16
+ __version_tuple__ = version_tuple = (0, 2, 0)