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.
- chromatic_python-0.2.1/PKG-INFO +147 -0
- chromatic_python-0.2.1/README.md +102 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/__init__.py +7 -4
- chromatic_python-0.2.1/chromatic/_typing.py +399 -0
- chromatic_python-0.2.1/chromatic/_version.py +21 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/ascii/_array.py +90 -123
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/ascii/_curses.py +18 -18
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/ascii/_glyph_proc.py +6 -12
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/colorconv.py +25 -36
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/core.py +332 -357
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/core.pyi +82 -89
- chromatic_python-0.2.1/chromatic/color/iterators.py +166 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/palette.py +117 -281
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/palette.pyi +63 -43
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/demo.py +12 -39
- chromatic_python-0.2.1/chromatic_python.egg-info/PKG-INFO +147 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic_python.egg-info/SOURCES.txt +3 -0
- chromatic_python-0.2.1/logo/logo.ANS +25 -0
- chromatic_python-0.2.1/logo/logo.PNG +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/pyproject.toml +20 -8
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/tests/test_color_str.py +49 -26
- chromatic_python-0.1.1/PKG-INFO +0 -56
- chromatic_python-0.1.1/README.md +0 -16
- chromatic_python-0.1.1/chromatic/_typing.py +0 -187
- chromatic_python-0.1.1/chromatic/_version.py +0 -16
- chromatic_python-0.1.1/chromatic_python.egg-info/PKG-INFO +0 -56
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/.gitattributes +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/.gitignore +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/LICENSE +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/ascii/__init__.py +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/color/__init__.py +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/__init__.py +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/__init__.pyi +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/fonts/IBM_VGA_437_8x16.ttf +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/fonts/consolas.ttf +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/images/butterfly.jpg +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/images/escher.png +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/images/goblin_virus.png +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic/data/images/hotdog.jpg +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic_python.egg-info/dependency_links.txt +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic_python.egg-info/requires.txt +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/chromatic_python.egg-info/top_level.txt +0 -0
- {chromatic_python-0.1.1 → chromatic_python-0.2.1}/setup.cfg +0 -0
- {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
|
+

|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/chromatic-python/)
|
|
49
|
+

|
|
50
|
+
[](https://pepy.tech/projects/chromatic-python)
|
|
51
|
+
[](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
|
+

|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/chromatic-python/)
|
|
4
|
+

|
|
5
|
+
[](https://pepy.tech/projects/chromatic-python)
|
|
6
|
+
[](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
|
-
|
|
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)
|