chromatic-python 0.1.1__py3-none-any.whl → 0.2.1__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
@@ -5,6 +5,7 @@ except ImportError:
5
5
 
6
6
  from . import ascii, color, data
7
7
  from .ascii import (
8
+ AnsiImage,
8
9
  ansi2img,
9
10
  ansi_quantize,
10
11
  ascii2img,
@@ -23,16 +24,18 @@ from .ascii import (
23
24
  )
24
25
  from .ascii._glyph_proc import get_glyph_masks
25
26
  from .color import (
26
- ansicolor24Bit,
27
- ansicolor4Bit,
28
- ansicolor8Bit,
29
27
  Back,
30
28
  Color,
31
- colorbytes,
29
+ ColorNamespace,
32
30
  ColorStr,
33
31
  Fore,
34
32
  SgrParameter,
35
33
  Style,
34
+ ansicolor24Bit,
35
+ ansicolor4Bit,
36
+ ansicolor8Bit,
37
+ colorbytes,
38
+ named_color,
36
39
  )
37
40
  from .data import register_user_font
38
41
 
chromatic/_typing.py CHANGED
@@ -1,30 +1,44 @@
1
1
  from __future__ import annotations
2
2
 
3
- 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
4
9
  from numbers import Number
5
- from types import UnionType
10
+ from operator import attrgetter, or_ as bitwise_or
6
11
  from typing import (
7
12
  Any,
8
13
  Callable,
9
14
  Concatenate,
10
- get_args,
11
- get_origin,
12
- get_type_hints,
15
+ Hashable,
13
16
  Iterable,
14
17
  Literal,
18
+ NamedTuple,
19
+ Optional,
15
20
  ParamSpec,
16
21
  Protocol,
17
22
  Sequence,
18
- SupportsRound,
19
- TYPE_CHECKING,
23
+ Type,
24
+ TypeAlias,
25
+ TypeAliasType,
26
+ TypeGuard,
20
27
  TypeVar,
28
+ TypedDict,
21
29
  Union,
30
+ Unpack,
31
+ cast,
32
+ get_args,
33
+ get_origin,
34
+ get_type_hints,
35
+ runtime_checkable,
22
36
  )
23
37
 
24
- from numpy import dtype, float64, generic, ndarray, number, uint8
25
- from numpy._typing import _ArrayLike, NDArray
26
38
  from PIL.Image import Image
27
39
  from PIL.ImageFont import FreeTypeFont
40
+ from numpy import dtype, float64, generic, ndarray, number, uint8
41
+ from numpy._typing import NDArray, _ArrayLike
28
42
 
29
43
  from chromatic.data import UserFont
30
44
 
@@ -34,45 +48,50 @@ _T_co = TypeVar('_T_co', covariant=True)
34
48
  _T_contra = TypeVar('_T_contra', contravariant=True)
35
49
  _AnyNumber_co = TypeVar('_AnyNumber_co', number, Number, covariant=True)
36
50
 
37
- if TYPE_CHECKING:
38
- from _typeshed import SupportsRichComparison, SupportsDivMod
39
-
40
- class SupportsRoundAndDivMod(
41
- SupportsRound[_T_co], SupportsDivMod[Any, _T_co], Protocol
42
- ): ...
43
-
44
-
45
- type ArrayReducerFunc[_SCT: generic] = Callable[
46
- Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]
47
- ]
48
- type KeyFunction[_T] = Callable[[_T], SupportsRichComparison]
49
- type ShapedNDArray[_Shape: tuple[int, ...], _SCT: generic] = ndarray[
50
- _Shape, dtype[_SCT]
51
- ]
51
+ type ArrayReducerFunc[_SCT: generic] = Callable[Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]]
52
+ type ShapedNDArray[_Shape: tuple[int, ...], _SCT: generic] = ndarray[_Shape, dtype[_SCT]]
52
53
  type MatrixLike[_SCT: generic] = ShapedNDArray[TupleOf2[int], _SCT]
53
54
  type SquareMatrix[_I: int, _SCT: generic] = ShapedNDArray[TupleOf2[_I], _SCT]
54
55
  type GlyphArray[_SCT: generic] = SquareMatrix[Literal[24], _SCT]
55
56
  type TupleOf2[_T] = tuple[_T, _T]
56
57
  type TupleOf3[_T] = tuple[_T, _T, _T]
57
- Float3Tuple = TupleOf3[float]
58
- Int3Tuple = TupleOf3[int]
59
- FloatSequence = Sequence[float]
60
- IntSequence = Sequence[int]
61
- GlyphBitmask = GlyphArray[bool]
62
- Bitmask = MatrixLike[bool]
63
- GreyscaleGlyphArray = GlyphArray[float64]
64
- GreyscaleArray = MatrixLike[float64]
65
- RGBArray = ShapedNDArray[tuple[int, int, Literal[3]], uint8]
66
- RGBPixel = ShapedNDArray[tuple[Literal[3]], uint8]
67
-
68
- RGBImageLike = Union[Image, RGBArray]
69
- 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
70
72
  ColorDictKeys = Literal['fg', 'bg']
71
73
  Ansi4BitAlias = Literal['4b']
72
74
  Ansi8BitAlias = Literal['8b']
73
75
  Ansi24BitAlias = Literal['24b']
74
76
  AnsiColorAlias = Ansi4BitAlias | Ansi8BitAlias | Ansi24BitAlias
75
- 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"
76
95
 
77
96
 
78
97
  def is_matching_type(value, typ):
@@ -85,10 +104,7 @@ def is_matching_type(value, typ):
85
104
  return value in args
86
105
  elif isinstance(typ, TypeVar):
87
106
  if typ.__constraints__:
88
- return any(
89
- is_matching_type(value, constraint)
90
- for constraint in typ.__constraints__
91
- )
107
+ return any(is_matching_type(value, constraint) for constraint in typ.__constraints__)
92
108
  else:
93
109
  return True
94
110
  elif origin is type:
@@ -134,54 +150,250 @@ def is_matching_type(value, typ):
134
150
  return False
135
151
 
136
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
+
137
173
  def deconstruct_type(tp):
138
174
  origin = get_origin(tp) or tp
139
175
  args = get_args(tp)
140
176
  return origin, args
141
177
 
142
178
 
143
- def is_matching_callable(value, expected_type):
144
- if not callable(value):
145
- return False
146
- return id(value) == id(expected_type)
179
+ @runtime_checkable
180
+ class SupportsUnion(Protocol[_T_contra, _T_co]):
147
181
 
182
+ def __or__(self, x: _T_contra, /) -> _T_co: ...
148
183
 
149
- def pseudo_union(ts: Iterable[type]) -> Union[type, UnionType]:
150
- return reduce(lambda x, y: x | y, ts)
151
184
 
185
+ def unionize(__iterable: Iterable[SupportsUnion[_T_contra, _T_co]]) -> _T_co:
186
+ return reduce(bitwise_or, __iterable)
152
187
 
153
- def type_error_msg(err_obj, *expected, context: str = '', obj_repr=False):
154
- n_expected = len(expected)
155
- name_slots = [f"{{{n}.__qualname__!r}}" for n in range(n_expected)]
156
- if name_slots and n_expected > 1:
157
- name_slots[-1] = f"or {name_slots[-1]}"
158
- names = (
159
- (', ' if n_expected > 2 else ' ')
160
- .join([context.strip()] + name_slots)
161
- .format(*expected)
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
194
+
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))
162
235
  )
163
- if not obj_repr:
164
- if not isinstance(err_obj, type):
165
- err_obj = type(err_obj)
166
- oops = repr(err_obj.__qualname__)
167
- elif not isinstance(err_obj, str):
168
- oops = repr(err_obj)
169
- else:
170
- oops = err_obj
171
- return f"expected {names}, got {oops} instead"
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))
172
252
 
173
253
 
174
- def is_matching_typed_dict(__d: dict, typed_dict: type[dict]) -> tuple[bool, str]:
175
- if not isinstance(__d, dict):
176
- return False, type_error_msg(__d, dict)
177
- expected = get_type_hints(typed_dict)
178
- if unexpected := __d.keys() - expected:
179
- return False, f"unexpected keyword arguments: {unexpected}"
180
- if missing := set(getattr(typed_dict, '__required_keys__', expected)) - __d.keys():
181
- return False, f"missing required keys: {missing}"
182
- for name, typ in expected.items():
183
- if ((field := __d.get(name)) is not None) and not is_matching_type(field, typ):
184
- return False, type_error_msg(
185
- field, typ, context=f'keyword argument {name!r} of type'
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)
186
291
  )
187
- return True, ''
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 CHANGED
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.1.1'
16
- __version_tuple__ = version_tuple = (0, 1, 1)
20
+ __version__ = version = '0.2.1'
21
+ __version_tuple__ = version_tuple = (0, 2, 1)