chromatic-python 0.1.1__tar.gz → 0.2.1__tar.gz

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.
Files changed (44) hide show
  1. chromatic_python-0.2.1/PKG-INFO +147 -0
  2. chromatic_python-0.2.1/README.md +102 -0
  3. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/__init__.py +7 -4
  4. chromatic_python-0.2.1/chromatic/_typing.py +399 -0
  5. chromatic_python-0.2.1/chromatic/_version.py +21 -0
  6. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/ascii/_array.py +90 -123
  7. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/ascii/_curses.py +18 -18
  8. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/ascii/_glyph_proc.py +6 -12
  9. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/colorconv.py +25 -36
  10. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/core.py +332 -357
  11. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/core.pyi +82 -89
  12. chromatic_python-0.2.1/chromatic/color/iterators.py +166 -0
  13. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/palette.py +117 -281
  14. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/palette.pyi +63 -43
  15. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/demo.py +12 -39
  16. chromatic_python-0.2.1/chromatic_python.egg-info/PKG-INFO +147 -0
  17. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic_python.egg-info/SOURCES.txt +3 -0
  18. chromatic_python-0.2.1/logo/logo.ANS +25 -0
  19. chromatic_python-0.2.1/logo/logo.PNG +0 -0
  20. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/pyproject.toml +20 -8
  21. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/tests/test_color_str.py +49 -26
  22. chromatic_python-0.1.1/PKG-INFO +0 -56
  23. chromatic_python-0.1.1/README.md +0 -16
  24. chromatic_python-0.1.1/chromatic/_typing.py +0 -187
  25. chromatic_python-0.1.1/chromatic/_version.py +0 -16
  26. chromatic_python-0.1.1/chromatic_python.egg-info/PKG-INFO +0 -56
  27. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/.gitattributes +0 -0
  28. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/.gitignore +0 -0
  29. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/LICENSE +0 -0
  30. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/ascii/__init__.py +0 -0
  31. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/__init__.py +0 -0
  32. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/__init__.py +0 -0
  33. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/__init__.pyi +0 -0
  34. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/fonts/IBM_VGA_437_8x16.ttf +0 -0
  35. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/fonts/consolas.ttf +0 -0
  36. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/images/butterfly.jpg +0 -0
  37. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/images/escher.png +0 -0
  38. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/images/goblin_virus.png +0 -0
  39. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/images/hotdog.jpg +0 -0
  40. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic_python.egg-info/dependency_links.txt +0 -0
  41. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic_python.egg-info/requires.txt +0 -0
  42. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic_python.egg-info/top_level.txt +0 -0
  43. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/setup.cfg +0 -0
  44. {chromatic_python-0.1.1 → chromatic_python-0.2.1}/tests/__init__.py +0 -0
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: chromatic-python
3
+ Version: 0.2.1
4
+ Summary: ANSI art image processing and colored terminal text
5
+ Author: crypt0lith
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 crypt0lith
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://github.com/crypt0lith/chromatic
28
+ Keywords: ansi,ascii,art,font,image,terminal,parser
29
+ Classifier: Programming Language :: Python :: 3.12
30
+ Classifier: Operating System :: OS Independent
31
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
32
+ Classifier: Typing :: Typed
33
+ Requires-Python: >=3.12
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: fonttools~=4.51.0
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
44
+ Dynamic: license-file
45
+
46
+ ![image](/logo/logo.PNG)
47
+
48
+ [![image](https://img.shields.io/pypi/v/chromatic-python)](https://pypi.org/project/chromatic-python/)
49
+ ![image](https://img.shields.io/pypi/pyversions/chromatic-python)
50
+ [![image](https://static.pepy.tech/badge/chromatic-python)](https://pepy.tech/projects/chromatic-python)
51
+ [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
52
+
53
+ Chromatic is a library for processing and transforming ANSI escape sequences (colored terminal text).
54
+
55
+ It offers a collection of algorithms and types for a variety of use cases:
56
+ - Image-to-ASCII / Image-to-ANSI conversions.
57
+ - ANSI art rendering, with support for user-defined fonts.
58
+ - A `ColorStr` type which enables precise low-level control over ANSI-escaped strings through a convenient interface.
59
+ - [colorama](https://github.com/tartley/colorama/)-style wrappers (`Fore`, `Back`, `Style`).
60
+ - Parametrization of ANSI color bit formats, allowing arbitrary conversion between 16-color, 256-color, and true-color (RGB) colorspace on any object implementing a `colorbytes` buffer.
61
+ - Et Cetera 😲
62
+
63
+ ### Usage
64
+ #### ColorStr
65
+ ```python
66
+ from chromatic import ColorStr
67
+
68
+ base_str = 'hello world'
69
+
70
+ red_fg = ColorStr(base_str, 0xFF0000)
71
+
72
+ assert red_fg.base_str == base_str
73
+ assert red_fg.rgb_dict == {'fg': (0xFF, 0, 0)}
74
+ assert red_fg.ansi == b'\x1b[38;5;196m'
75
+ ```
76
+
77
+ `ColorStr` can handle different signatures for `color_spec`:
78
+ ```python
79
+ from chromatic import ColorStr
80
+
81
+ assert all(
82
+ ColorStr('*', cs) == ColorStr('*', 0xFF0000)
83
+ for cs in [b'\x1b[38;5;196m', b'\xff\x00\x00', (0xFF, 0, 0), {'fg': 0xFF0000}]
84
+ )
85
+ ```
86
+
87
+ The ANSI color format can be given as an argument, or returned by `ColorStr.as_ansi_type()` as a new instance.
88
+ ```python
89
+ from chromatic import ColorStr, ansicolor24Bit, ansicolor4Bit
90
+
91
+ truecolor = ColorStr('*', 0xFF0000, ansi_type=ansicolor24Bit)
92
+ a_16color = truecolor.as_ansi_type(ansicolor4Bit)
93
+
94
+ # each ansi color format has an alias that can be used in place of the type object
95
+ assert a_16color == truecolor.as_ansi_type('4b')
96
+
97
+ assert truecolor.ansi_format is ansicolor24Bit and truecolor.ansi == b'\x1b[38;2;255;0;0m'
98
+ assert a_16color.ansi_format is ansicolor4Bit and a_16color.ansi == b'\x1b[31m'
99
+ ```
100
+
101
+ Adding and removing specific ANSI codes from the escape sequence:
102
+ ```python
103
+ import chromatic as cm
104
+
105
+ boring_str = cm.ColorStr('hello world')
106
+
107
+ assert boring_str.ansi == b''
108
+
109
+ bold_str = boring_str + cm.SgrParameter.BOLD
110
+ assert bold_str.ansi == b'\x1b[1m'
111
+
112
+ # use ColorStr.update_sgr() to remove and add SGR values
113
+ unbold_str = bold_str.update_sgr(cm.SgrParameter.BOLD)
114
+ assert unbold_str == boring_str
115
+ assert bold_str == unbold_str.update_sgr(cm.SgrParameter.BOLD)
116
+ ```
117
+
118
+ #### Image-to-ANSI conversion
119
+
120
+ Converting an image into an array of ANSI-escaped characters, then rendering the ANSI array as another image:
121
+ ```python
122
+ from chromatic.color import ansicolor4Bit
123
+ from chromatic.ascii import ansi2img, img2ansi
124
+ from chromatic.data import UserFont, butterfly
125
+
126
+ input_img = butterfly()
127
+
128
+ font = UserFont.IBM_VGA_437_8X16
129
+
130
+ # by default, `char_set` would be sorted based on the relative weight of glyphs in the font
131
+ # but because `sort_glyphs` is set to False, `char_set` will be directly mapped to the image brightness
132
+ # | <- index 0 is the 'darkest'
133
+ char_set = r"'·,•-_→+<>ⁿ*%⌂7√Iï∞πbz£9yîU{}1αHSw♥æ?GX╕╒éà⌡MF╝╩ΘûǃQ½☻Ŷ┤▄╪║▒█"
134
+ # index -1 is the 'brightest' -> |
135
+
136
+ ansi_array = img2ansi(input_img, font, sort_glyphs=False, char_set=char_set, ansi_type=ansicolor4Bit, factor=200)
137
+
138
+ # ansi2img() returns a PIL.Image.Image object
139
+ ansi_img = ansi2img(ansi_array, font, font_size=16)
140
+ ansi_img.show()
141
+ ```
142
+
143
+ ### Installation
144
+ Install the package using `pip`:
145
+ ```bash
146
+ pip install chromatic-python
147
+ ```
@@ -0,0 +1,102 @@
1
+ ![image](/logo/logo.PNG)
2
+
3
+ [![image](https://img.shields.io/pypi/v/chromatic-python)](https://pypi.org/project/chromatic-python/)
4
+ ![image](https://img.shields.io/pypi/pyversions/chromatic-python)
5
+ [![image](https://static.pepy.tech/badge/chromatic-python)](https://pepy.tech/projects/chromatic-python)
6
+ [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
7
+
8
+ Chromatic is a library for processing and transforming ANSI escape sequences (colored terminal text).
9
+
10
+ It offers a collection of algorithms and types for a variety of use cases:
11
+ - Image-to-ASCII / Image-to-ANSI conversions.
12
+ - ANSI art rendering, with support for user-defined fonts.
13
+ - A `ColorStr` type which enables precise low-level control over ANSI-escaped strings through a convenient interface.
14
+ - [colorama](https://github.com/tartley/colorama/)-style wrappers (`Fore`, `Back`, `Style`).
15
+ - Parametrization of ANSI color bit formats, allowing arbitrary conversion between 16-color, 256-color, and true-color (RGB) colorspace on any object implementing a `colorbytes` buffer.
16
+ - Et Cetera 😲
17
+
18
+ ### Usage
19
+ #### ColorStr
20
+ ```python
21
+ from chromatic import ColorStr
22
+
23
+ base_str = 'hello world'
24
+
25
+ red_fg = ColorStr(base_str, 0xFF0000)
26
+
27
+ assert red_fg.base_str == base_str
28
+ assert red_fg.rgb_dict == {'fg': (0xFF, 0, 0)}
29
+ assert red_fg.ansi == b'\x1b[38;5;196m'
30
+ ```
31
+
32
+ `ColorStr` can handle different signatures for `color_spec`:
33
+ ```python
34
+ from chromatic import ColorStr
35
+
36
+ assert all(
37
+ ColorStr('*', cs) == ColorStr('*', 0xFF0000)
38
+ for cs in [b'\x1b[38;5;196m', b'\xff\x00\x00', (0xFF, 0, 0), {'fg': 0xFF0000}]
39
+ )
40
+ ```
41
+
42
+ The ANSI color format can be given as an argument, or returned by `ColorStr.as_ansi_type()` as a new instance.
43
+ ```python
44
+ from chromatic import ColorStr, ansicolor24Bit, ansicolor4Bit
45
+
46
+ truecolor = ColorStr('*', 0xFF0000, ansi_type=ansicolor24Bit)
47
+ a_16color = truecolor.as_ansi_type(ansicolor4Bit)
48
+
49
+ # each ansi color format has an alias that can be used in place of the type object
50
+ assert a_16color == truecolor.as_ansi_type('4b')
51
+
52
+ assert truecolor.ansi_format is ansicolor24Bit and truecolor.ansi == b'\x1b[38;2;255;0;0m'
53
+ assert a_16color.ansi_format is ansicolor4Bit and a_16color.ansi == b'\x1b[31m'
54
+ ```
55
+
56
+ Adding and removing specific ANSI codes from the escape sequence:
57
+ ```python
58
+ import chromatic as cm
59
+
60
+ boring_str = cm.ColorStr('hello world')
61
+
62
+ assert boring_str.ansi == b''
63
+
64
+ bold_str = boring_str + cm.SgrParameter.BOLD
65
+ assert bold_str.ansi == b'\x1b[1m'
66
+
67
+ # use ColorStr.update_sgr() to remove and add SGR values
68
+ unbold_str = bold_str.update_sgr(cm.SgrParameter.BOLD)
69
+ assert unbold_str == boring_str
70
+ assert bold_str == unbold_str.update_sgr(cm.SgrParameter.BOLD)
71
+ ```
72
+
73
+ #### Image-to-ANSI conversion
74
+
75
+ Converting an image into an array of ANSI-escaped characters, then rendering the ANSI array as another image:
76
+ ```python
77
+ from chromatic.color import ansicolor4Bit
78
+ from chromatic.ascii import ansi2img, img2ansi
79
+ from chromatic.data import UserFont, butterfly
80
+
81
+ input_img = butterfly()
82
+
83
+ font = UserFont.IBM_VGA_437_8X16
84
+
85
+ # by default, `char_set` would be sorted based on the relative weight of glyphs in the font
86
+ # but because `sort_glyphs` is set to False, `char_set` will be directly mapped to the image brightness
87
+ # | <- index 0 is the 'darkest'
88
+ char_set = r"'·,•-_→+<>ⁿ*%⌂7√Iï∞πbz£9yîU{}1αHSw♥æ?GX╕╒éà⌡MF╝╩ΘûǃQ½☻Ŷ┤▄╪║▒█"
89
+ # index -1 is the 'brightest' -> |
90
+
91
+ ansi_array = img2ansi(input_img, font, sort_glyphs=False, char_set=char_set, ansi_type=ansicolor4Bit, factor=200)
92
+
93
+ # ansi2img() returns a PIL.Image.Image object
94
+ ansi_img = ansi2img(ansi_array, font, font_size=16)
95
+ ansi_img.show()
96
+ ```
97
+
98
+ ### Installation
99
+ Install the package using `pip`:
100
+ ```bash
101
+ pip install chromatic-python
102
+ ```
@@ -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
 
@@ -0,0 +1,399 @@
1
+ from __future__ import annotations
2
+
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
9
+ from numbers import Number
10
+ from operator import attrgetter, or_ as bitwise_or
11
+ from typing import (
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,
36
+ )
37
+
38
+ from PIL.Image import Image
39
+ from PIL.ImageFont import FreeTypeFont
40
+ from numpy import dtype, float64, generic, ndarray, number, uint8
41
+ from numpy._typing import NDArray, _ArrayLike
42
+
43
+ from chromatic.data import UserFont
44
+
45
+ _P = ParamSpec('_P')
46
+ _T = TypeVar('_T')
47
+ _T_co = TypeVar('_T_co', covariant=True)
48
+ _T_contra = TypeVar('_T_contra', contravariant=True)
49
+ _AnyNumber_co = TypeVar('_AnyNumber_co', number, Number, covariant=True)
50
+
51
+ type ArrayReducerFunc[_SCT: generic] = Callable[Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]]
52
+ type ShapedNDArray[_Shape: tuple[int, ...], _SCT: generic] = ndarray[_Shape, dtype[_SCT]]
53
+ type MatrixLike[_SCT: generic] = ShapedNDArray[TupleOf2[int], _SCT]
54
+ type SquareMatrix[_I: int, _SCT: generic] = ShapedNDArray[TupleOf2[_I], _SCT]
55
+ type GlyphArray[_SCT: generic] = SquareMatrix[Literal[24], _SCT]
56
+ type TupleOf2[_T] = tuple[_T, _T]
57
+ type TupleOf3[_T] = tuple[_T, _T, _T]
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
72
+ ColorDictKeys = Literal['fg', 'bg']
73
+ Ansi4BitAlias = Literal['4b']
74
+ Ansi8BitAlias = Literal['8b']
75
+ Ansi24BitAlias = Literal['24b']
76
+ AnsiColorAlias = Ansi4BitAlias | Ansi8BitAlias | Ansi24BitAlias
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"
95
+
96
+
97
+ def is_matching_type(value, typ):
98
+ if typ is Any:
99
+ return True
100
+ origin, args = deconstruct_type(typ)
101
+ if origin is Union:
102
+ return any(is_matching_type(value, arg) for arg in args)
103
+ elif origin is Literal:
104
+ return value in args
105
+ elif isinstance(typ, TypeVar):
106
+ if typ.__constraints__:
107
+ return any(is_matching_type(value, constraint) for constraint in typ.__constraints__)
108
+ else:
109
+ return True
110
+ elif origin is type:
111
+ if not isinstance(value, type):
112
+ return False
113
+ target_type = args[0]
114
+ target_origin = get_origin(target_type)
115
+ target_args = get_args(target_type)
116
+ if target_origin is Union:
117
+ return any(issubclass(value, t) for t in target_args)
118
+ else:
119
+ return issubclass(value, target_type)
120
+ elif origin is Callable:
121
+ return is_matching_callable(value, typ)
122
+ elif origin is list:
123
+ if not isinstance(value, list):
124
+ return False
125
+ if not args:
126
+ return True
127
+ return all(is_matching_type(item, args[0]) for item in value)
128
+ elif origin is dict:
129
+ if not isinstance(value, dict):
130
+ return False
131
+ if not args:
132
+ return True
133
+ key_type, val_type = args
134
+ return all(
135
+ is_matching_type(k, key_type) and is_matching_type(v, val_type)
136
+ for k, v in value.items()
137
+ )
138
+ elif origin is tuple:
139
+ if not isinstance(value, tuple):
140
+ return False
141
+ if len(args) == 2 and args[1] is ...:
142
+ return all(is_matching_type(item, args[0]) for item in value)
143
+ if len(value) != len(args):
144
+ return False
145
+ return all(is_matching_type(v, t) for v, t in zip(value, args))
146
+ else:
147
+ try:
148
+ return isinstance(value, typ)
149
+ except TypeError:
150
+ return False
151
+
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
+
173
+ def deconstruct_type(tp):
174
+ origin = get_origin(tp) or tp
175
+ args = get_args(tp)
176
+ return origin, args
177
+
178
+
179
+ @runtime_checkable
180
+ class SupportsUnion(Protocol[_T_contra, _T_co]):
181
+
182
+ def __or__(self, x: _T_contra, /) -> _T_co: ...
183
+
184
+
185
+ def unionize(__iterable: Iterable[SupportsUnion[_T_contra, _T_co]]) -> _T_co:
186
+ return reduce(bitwise_or, __iterable)
187
+
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))
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, {})
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.2.1'
21
+ __version_tuple__ = version_tuple = (0, 2, 1)