zstdlib 0.0.1__tar.gz → 0.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zstdlib
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: A set of useful python utilities
5
5
  License: GPLv3
6
6
  Project-URL: Homepage, https://github.com/zwimer/zstdlib
@@ -12,3 +12,6 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
+
16
+ # zstdlib
17
+ A set of useful python utilities
@@ -0,0 +1,2 @@
1
+ # zstdlib
2
+ A set of useful python utilities
@@ -49,11 +49,16 @@ version = {attr = "zstdlib.__version__"}
49
49
 
50
50
  # Tools
51
51
 
52
+ [tool.pylint.MASTER]
53
+ ignore-paths = '^tests/.*$'
52
54
  [tool.pylint."MESSAGES CONTROL"]
53
55
  disable = [
56
+ "unnecessary-lambda-assignment",
57
+ "method-cache-max-size-none",
54
58
  "missing-module-docstring",
55
- "invalid-name",
56
- "line-too-long"
59
+ "too-few-public-methods",
60
+ "line-too-long",
61
+ "invalid-name"
57
62
  ]
58
63
 
59
64
  [tool.black]
@@ -71,6 +76,6 @@ ignore=["E731"]
71
76
  skips = ["B101", "B104", "B201"]
72
77
 
73
78
  [tool.vulture]
74
- ignore_names = ["cli", "_help", "_show_version", "_channel", "strict_slashes"]
79
+ ignore_names = []
75
80
  min_confidence = 70
76
81
  paths = ["zstdlib"]
File without changes
File without changes
@@ -0,0 +1,69 @@
1
+ from logging.handlers import QueueHandler
2
+ from contextlib import contextmanager
3
+ from collections.abc import Callable
4
+ from threading import Lock
5
+ from typing import Any
6
+ import logging
7
+ import queue
8
+
9
+
10
+ @contextmanager
11
+ def _cml(logger: logging.Logger, exit_func: Callable[[], Any]):
12
+ try:
13
+ yield logger
14
+ finally:
15
+ exit_func()
16
+
17
+
18
+ class LeftBase:
19
+ """
20
+ A base class for tests that hijack loggers
21
+ """
22
+
23
+ _hijacked: set[logging.Logger] = set() # Set of all loggers that have ever been hijacked
24
+ _qmap: dict[logging.Logger, queue.Queue[logging.LogRecord]] = {}
25
+ _lb_old: dict[logging.Logger, list[logging.Handler]] = {}
26
+ _lb_lock = Lock()
27
+
28
+ messages: dict[logging.Logger, list[str]] = {}
29
+
30
+ @classmethod
31
+ def hijack(cls, name: str, reuse: bool = False, fmt: logging.Formatter | None = None):
32
+ """
33
+ :return: A logger configured to write to an internal queue
34
+ """
35
+ log = logging.getLogger(name)
36
+ q: queue.Queue[logging.LogRecord] = queue.Queue()
37
+ with cls._lb_lock:
38
+ if not reuse and log in cls._hijacked:
39
+ raise RuntimeError("Logger already hijacked")
40
+ cls._hijacked.add(log)
41
+ cls._lb_old[log] = log.handlers
42
+ cls._qmap[log] = q
43
+ log.handlers = [QueueHandler(q)]
44
+ if fmt is not None:
45
+ log.handlers[0].setFormatter(fmt)
46
+ log.setLevel(1) # Not 0 to ensure that the parent logger level is not used
47
+ return _cml(log, lambda: cls._restore(log))
48
+
49
+ @classmethod
50
+ def _restore(cls, logger: logging.Logger) -> None:
51
+ """
52
+ Restore the logger and read all messages from the queue
53
+ :return: The messages stored by the logger queue
54
+ """
55
+ if len(logger.handlers) != 1 or not isinstance(qh := logger.handlers[0], QueueHandler):
56
+ raise RuntimeError("Logger not hijacked")
57
+ # Restore old logger and extract q
58
+ with cls._lb_lock:
59
+ if logger not in cls._hijacked:
60
+ raise RuntimeError("Logger not hijacked")
61
+ logger.handlers = cls._lb_old.pop(logger)
62
+ q = cls._qmap.pop(logger)
63
+ # Read queue
64
+ qh.flush()
65
+ messages = []
66
+ while not q.empty():
67
+ messages.append(q.get(False).msg)
68
+ with cls._lb_lock:
69
+ cls.messages[logger] = messages
@@ -0,0 +1,98 @@
1
+ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,unused-variable
2
+ import unittest
3
+
4
+ from zstdlib.log import CuteFormatter
5
+ from zstdlib.ansi import Color
6
+
7
+ from .base import LeftBase
8
+
9
+
10
+ class TestCuteFormatter(LeftBase, unittest.TestCase):
11
+
12
+ def test_hardcoded_colors(self) -> None:
13
+ color = Color.strikethrough_green
14
+ name = "TestCuteFormatter.test_hardcoded_colors"
15
+ with self.hijack(name, fmt=CuteFormatter(colors={name: color})) as log:
16
+ log.debug("test")
17
+ msg = self.messages[log][0].rsplit("|", 1)[-1]
18
+ self.assertEqual(f" {color.code}test{Color.RESET}", msg)
19
+
20
+ def test_name_width(self) -> None:
21
+ width = 123
22
+ color = Color.red
23
+ name = "TestCuteFormatter.name_width"
24
+ with self.hijack(name, fmt=CuteFormatter(colors={name: color}, name_width=width)) as log:
25
+ log.debug("test")
26
+ lvl, _, nam, msg = self.messages[log][0].split("|")
27
+ self.assertEqual("DEBUG", lvl.strip())
28
+ self.assertEqual(f" {color(name.ljust(width))} ", nam)
29
+ self.assertEqual(f" {color('test')}", msg)
30
+
31
+ def test_no_color(self) -> None:
32
+ name = "TestCuteFormatter.test_no_color"
33
+ with self.hijack(name, fmt=CuteFormatter(colored=False)) as log:
34
+ log.debug("test")
35
+ lvl, _, nam, msg = self.messages[log][0].split("|")
36
+ self.assertEqual("DEBUG", lvl.strip())
37
+ self.assertEqual(name, nam.strip())
38
+ self.assertEqual(" test", msg)
39
+
40
+ def test_edit_colors(self) -> None:
41
+ name = "TestCuteFormatter.edit_colors"
42
+ cf = CuteFormatter(colored=False)
43
+ color = Color.strikethrough_red
44
+ with self.hijack(name, fmt=cf) as log:
45
+ log.critical("test")
46
+ msg = self.messages[log][0].rsplit("|", 1)[-1]
47
+ self.assertEqual(" test", msg)
48
+ with self.hijack(name, reuse=True, fmt=cf) as log:
49
+ cf.colored = True
50
+ cf.update({name: color})
51
+ log.critical("test")
52
+ msg = self.messages[log][0].rsplit("|", 1)[-1]
53
+ self.assertEqual(f" {color.code}test{Color.RESET}", msg)
54
+
55
+ def test_level_color(self) -> None:
56
+ name = "TestCuteFormatter.test_level_color"
57
+ msg = "test"
58
+ levels = (5, 10, 20, 30, 40, 50, 60)
59
+ with self.hijack(name, fmt=CuteFormatter()) as log:
60
+ for i in levels:
61
+ log.log(i, msg)
62
+ self.assertEqual(len(levels), len(self.messages[log]))
63
+ l_name = lambda idx: self.messages[log][idx].split("|")[0].strip()
64
+ self.assertEqual(Color.dim("Level 5".ljust(8)), l_name(0))
65
+ self.assertEqual("DEBUG", l_name(1))
66
+ self.assertEqual(Color.blue("INFO".ljust(8)), l_name(2))
67
+ self.assertEqual(Color.yellow("WARNING".ljust(8)), l_name(3))
68
+ self.assertEqual(Color.red("ERROR".ljust(8)), l_name(4))
69
+ self.assertEqual(Color.bright_red_bg_yellow("CRITICAL".ljust(8)), l_name(5))
70
+ self.assertEqual(Color.bright_red_bg_yellow("Level 60".ljust(8)), l_name(6))
71
+
72
+ def test_exception(self) -> None:
73
+ name = "TestCuteFormatter.test_exception"
74
+ with self.hijack(name, fmt=CuteFormatter()) as log:
75
+ try:
76
+ raise ValueError(name)
77
+ except ValueError:
78
+ log.error("test", exc_info=True)
79
+ spt = self.messages[log][0].split("\n")
80
+ self.assertTrue(len(spt) > 2)
81
+ self.assertEqual("Traceback (most recent call last):", spt[1].strip())
82
+ self.assertEqual("raise ValueError(name)", spt[-2].strip())
83
+ self.assertEqual(f"ValueError: {name}", spt[-1].strip())
84
+
85
+ def test_multi_color(self) -> None:
86
+ base = "TestCuteFormatter.test_multi_color."
87
+ cf = CuteFormatter()
88
+ messages: set[str] = set()
89
+ for i in range(100):
90
+ with self.hijack(f"{base}{i}", fmt=cf) as log:
91
+ log.info(base)
92
+ messages.add(self.messages[log][0].rsplit("|", 1)[-1].strip())
93
+ cols = ("red", "green", "yellow", "blue", "magenta", "cyan", "default")
94
+ self.assertEqual({getattr(Color, i)(base) for i in cols}, messages)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ unittest.main()
@@ -0,0 +1,74 @@
1
+ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
2
+ import unittest
3
+ import logging
4
+
5
+ from zstdlib.log import trace
6
+
7
+ from .base import LeftBase
8
+
9
+
10
+ # mypy: disable_error_code="attr-defined"
11
+ class TestTrace(LeftBase, unittest.TestCase):
12
+
13
+ def test_trace(self) -> None:
14
+ """
15
+ Avoid splitting up into multiple functions since trace.install() affects global state
16
+ Keeping this as one function ensures that the tests are run in order
17
+ """
18
+ # Test bad installs
19
+ with self.assertRaises(ValueError):
20
+ trace.install(value=-1)
21
+ with self.assertRaises(ValueError):
22
+ trace.install(value=logging.DEBUG + 1)
23
+ logging.TRACE = None
24
+ with self.assertRaises(AttributeError):
25
+ trace.install()
26
+ del logging.TRACE
27
+ logging.trace = None
28
+ with self.assertRaises(AttributeError):
29
+ trace.install()
30
+ del logging.trace
31
+ logging.getLoggerClass().trace = None
32
+ with self.assertRaises(AttributeError):
33
+ trace.install()
34
+ del logging.getLoggerClass().trace
35
+ trace._State.start = True # pylint: disable=protected-access
36
+ with self.assertRaises(RuntimeError):
37
+ trace.install()
38
+ trace._State.start = False # pylint: disable=protected-access
39
+ # Good install
40
+ trace.install(value=5)
41
+ # Attribute check
42
+ self.assertTrue(hasattr(logging, "TRACE"))
43
+ self.assertTrue(hasattr(logging, "trace"))
44
+ self.assertTrue(hasattr(logging.getLogger(), "trace"))
45
+ self.assertEqual(logging.TRACE, 5)
46
+ self.assertEqual(logging.getLevelName(logging.TRACE), "TRACE") # type: ignore[call-overload]
47
+ # Root check
48
+ with self.hijack("") as log:
49
+ old = log.getEffectiveLevel()
50
+ # pylint: disable=not-callable
51
+ logging.trace("test1") # type: ignore[misc]
52
+ log.setLevel(logging.DEBUG)
53
+ # pylint: disable=not-callable
54
+ logging.trace("test2") # type: ignore[misc]
55
+ log.setLevel(old)
56
+ self.assertEqual(self.messages[log], ["test1"])
57
+ # Logger check
58
+ with self.hijack("TestTrace.t1") as log:
59
+ old = log.getEffectiveLevel()
60
+ # pylint: disable=not-callable
61
+ log.trace("test3")
62
+ log.setLevel(logging.DEBUG)
63
+ # pylint: disable=not-callable
64
+ log.trace("test4")
65
+ log.setLevel(old)
66
+ self.assertEqual(self.messages[log], ["test3"])
67
+ # Test re-installation
68
+ with self.assertRaises(RuntimeError):
69
+ trace.install(value=5)
70
+ trace.install(value=5, force=True)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ unittest.main()
@@ -0,0 +1,56 @@
1
+ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
2
+ import unittest
3
+
4
+ from zstdlib.ansi import PureColor, RawColor, Color
5
+
6
+
7
+ class TestAnsiColor(unittest.TestCase):
8
+
9
+ def test_raw_color(self) -> None:
10
+ msg = "Hello, World!"
11
+ for i, k in PureColor.__members__.items():
12
+ self.assertEqual(f"\033[{k.value}m{msg}\033[0m", getattr(Color, i)(msg))
13
+
14
+ def test_bright_background_color(self) -> None:
15
+ msg = "Hello, World!"
16
+ for i, k in PureColor.__members__.items():
17
+ self.assertEqual(f"\033[{k.value+70}m{msg}\033[0m", getattr(Color, f"bg_bright_{i}")(msg))
18
+
19
+ def test_init(self) -> None:
20
+ msg = "Hello, World!"
21
+ cname = "black"
22
+ c = RawColor(getattr(PureColor, cname))
23
+ self.assertEqual(f"\033[{c.value}m{msg}\033[0m", getattr(Color, cname)(msg)) # Sanity check
24
+ self.assertEqual(f"\033[{c.value}m{msg}\033[0m", Color(c)(msg))
25
+ self.assertEqual(f"\033[{c.value}m{msg}\033[0m", Color(c)(msg))
26
+ self.assertEqual(f"\033[{c.value}m{msg}\033[0m", Color(Color(c))(msg))
27
+ self.assertEqual(f"\033[{c.value}m{msg}\033[0m", Color(code=Color(c).code)(msg))
28
+ # Errors
29
+ with self.assertRaises(ValueError):
30
+ _ = Color("blue_blue")
31
+ with self.assertRaises(ValueError):
32
+ _ = Color("bg_blue_bg_bright_blue")
33
+ with self.assertRaises(ValueError):
34
+ _ = Color(fmt="1", code="1")
35
+ with self.assertRaises(ValueError):
36
+ _ = Color(code="1")
37
+ with self.assertRaises(ValueError):
38
+ _ = Color("bright_bg_red")
39
+ # Error converted
40
+ with self.assertRaises(AttributeError):
41
+ _ = Color.bright_bg_red
42
+
43
+ def test_modifiers(self) -> None:
44
+ msg = "Hello, World!"
45
+ self.assertEqual(f"\033[1;34m{msg}\033[0m", Color.bold_blue(msg)) # Small modifier
46
+ self.assertEqual(f"\033[9;32m{msg}\033[0m", Color.strikethrough_green(msg)) # Large modifier
47
+ self.assertEqual(f"\033[2;9;31m{msg}\033[0m", Color.strikethrough_dim_red(msg))
48
+ self.assertEqual(f"\033[3;4;5;39m{msg}\033[0m", Color.underline_italic_blinking_default(msg))
49
+ # Test no colors / multiple colors
50
+ self.assertEqual(f"\033[1m{msg}\033[0m", Color.bold(msg))
51
+ self.assertEqual(f"\033[31;42m{msg}\033[0m", Color.red_bg_green(msg))
52
+ self.assertEqual(f"\033[1;3;32;101m{msg}\033[0m", Color.bg_bright_red_bold_italic_green(msg))
53
+
54
+
55
+ if __name__ == "__main__":
56
+ unittest.main()
@@ -0,0 +1,137 @@
1
+ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,unused-variable
2
+ import unittest
3
+
4
+ from zstdlib import EnumType, Enum
5
+
6
+
7
+ class TestEnumType(unittest.TestCase):
8
+
9
+ def test_valid(self) -> None:
10
+ class ET1(metaclass=EnumType):
11
+ arg1: int = 0
12
+ arg2: int = 1
13
+
14
+ def test_empty(self) -> None:
15
+ class ET2(metaclass=EnumType, empty_ok=True):
16
+ pass
17
+
18
+ with self.assertRaises(ValueError):
19
+
20
+ class ET3(metaclass=EnumType):
21
+ pass
22
+
23
+ def test_dupe(self) -> None:
24
+ with self.assertRaises(ValueError):
25
+
26
+ class ET4(metaclass=EnumType):
27
+ arg1: int = 0
28
+ arg2: int = 0
29
+
30
+ def test_annotations(self) -> None:
31
+ with self.assertRaises(ValueError):
32
+
33
+ class ET5(metaclass=EnumType):
34
+ arg1 = 1
35
+
36
+ with self.assertRaises(ValueError):
37
+
38
+ class ET6(metaclass=EnumType):
39
+ arg1: int
40
+
41
+ with self.assertRaises(TypeError):
42
+
43
+ class ET7(metaclass=EnumType):
44
+ arg1: str = 0 # type: ignore[assignment]
45
+
46
+ def test_instantiation(self) -> None:
47
+ with self.assertRaises(AttributeError):
48
+
49
+ class ET8(metaclass=EnumType):
50
+ arg1: int = 1
51
+
52
+ def __init__(self):
53
+ pass
54
+
55
+ with self.assertRaises(AttributeError):
56
+
57
+ class ET9(metaclass=EnumType):
58
+ arg1: int = 1
59
+
60
+ def __new__(cls):
61
+ pass
62
+
63
+ class ET10(metaclass=EnumType):
64
+ arg1: int = 0
65
+
66
+ with self.assertRaises(NotImplementedError):
67
+ ET10()
68
+ with self.assertRaises(NotImplementedError):
69
+ ET10.__init__({})
70
+
71
+
72
+ class TestEnum(unittest.TestCase):
73
+ def test_valid(self) -> None:
74
+ class E1(Enum):
75
+ arg1: int = 0
76
+ arg2: int = 1
77
+
78
+ def test_empty(self) -> None:
79
+ class E2(Enum, empty_ok=True):
80
+ pass
81
+
82
+ with self.assertRaises(ValueError):
83
+
84
+ class E3(Enum):
85
+ pass
86
+
87
+ def test_dupe(self) -> None:
88
+ with self.assertRaises(ValueError):
89
+
90
+ class E4(Enum):
91
+ arg1: int = 0
92
+ arg2: int = 0
93
+
94
+ def test_annotations(self) -> None:
95
+ with self.assertRaises(ValueError):
96
+
97
+ class E5(Enum):
98
+ arg1 = 1
99
+
100
+ with self.assertRaises(ValueError):
101
+
102
+ class E6(Enum):
103
+ arg1: int
104
+
105
+ with self.assertRaises(TypeError):
106
+
107
+ class E7(Enum):
108
+ arg1: str = 0 # type: ignore[assignment]
109
+
110
+ def test_instantiation(self) -> None:
111
+ with self.assertRaises(AttributeError):
112
+
113
+ class E8(Enum):
114
+ arg1: int = 1
115
+
116
+ def __init__(self):
117
+ pass
118
+
119
+ with self.assertRaises(AttributeError):
120
+
121
+ class E9(Enum):
122
+ arg1: int = 1
123
+
124
+ def __new__(cls):
125
+ pass
126
+
127
+ class E10(Enum):
128
+ arg1: int = 0
129
+
130
+ with self.assertRaises(NotImplementedError):
131
+ E10()
132
+ with self.assertRaises(NotImplementedError):
133
+ E10.__init__({})
134
+
135
+
136
+ if __name__ == "__main__":
137
+ unittest.main()
@@ -0,0 +1,126 @@
1
+ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,unused-variable
2
+ from threading import Thread, Lock
3
+ from time import sleep
4
+ import unittest
5
+
6
+ from zstdlib import SingletonType, Singleton
7
+
8
+
9
+ class TestSingletonType(unittest.TestCase):
10
+
11
+ def test_valid(self) -> None:
12
+ class ST1(metaclass=SingletonType):
13
+ pass
14
+
15
+ self.assertIs(ST1(), ST1())
16
+
17
+ class ST2(metaclass=SingletonType):
18
+ pass
19
+
20
+ self.assertIsNot(ST1(), ST2())
21
+
22
+ def test_subclass(self) -> None:
23
+ class ST3(metaclass=SingletonType):
24
+ pass
25
+
26
+ with self.assertRaises(NotImplementedError):
27
+
28
+ class ST4(ST3):
29
+ pass
30
+
31
+ def test_multi_thread(self):
32
+ """
33
+ Ensure that SingletonType is thread safe and that constructing an object doesn't delay other threads
34
+ Technically this is more of a heuristic, but it failing is extremely unlikely
35
+ """
36
+
37
+ class ST5(metaclass=SingletonType):
38
+ def __init__(self):
39
+ sleep(0.2)
40
+
41
+ class ST6(metaclass=SingletonType):
42
+ def __init__(self):
43
+ sleep(0.8)
44
+
45
+ lock = Lock()
46
+ events = []
47
+ results = []
48
+
49
+ def t1() -> None:
50
+ """
51
+ Construct an ST6 immediately
52
+ """
53
+ with lock:
54
+ pass
55
+ events.append("START: ST6()")
56
+ results.append(ST6())
57
+ events.append("END: ST6()")
58
+
59
+ def t2() -> None:
60
+ """
61
+ Construct an ST6 after the first ST6 has started construction but before it has finished
62
+ """
63
+ with lock:
64
+ pass
65
+ sleep(0.2)
66
+ events.append("START: ST6()")
67
+ results.append(ST6())
68
+ events.append("END: ST6()")
69
+
70
+ def t3() -> None:
71
+ """
72
+ Construct ST5's after both ST6s have started construction, finishing before either end
73
+ """
74
+ with lock:
75
+ pass
76
+ sleep(0.4)
77
+ # Loop Enough times that ST6 wil be complete if it ST5 actually constructed each time
78
+ for i in range(10):
79
+ events.append("START: ST5()")
80
+ results.append(ST5())
81
+ events.append("END: ST5()")
82
+
83
+ threads = (Thread(target=t1), Thread(target=t2), Thread(target=t3))
84
+ with lock:
85
+ for i in threads:
86
+ i.start()
87
+ # Give threads a moment to construct then let them go
88
+ sleep(0.2)
89
+ for i in threads:
90
+ i.join()
91
+ # Check results
92
+ wanted = ["START: ST6()"] * 2 + ["START: ST5()", "END: ST5()"] * 10 + ["END: ST6()"] * 2
93
+ self.assertEqual(events, wanted)
94
+ # Check constructed objects
95
+ self.assertEqual(len(results), 2 + 10)
96
+ for i in range(9):
97
+ self.assertIs(results[0], results[i + 1])
98
+ self.assertIsNot(results[0], results[-1])
99
+ self.assertIs(results[-1], results[-2])
100
+
101
+
102
+ class TestSingleton(unittest.TestCase):
103
+
104
+ def test_valid(self):
105
+ class S1(Singleton):
106
+ pass
107
+
108
+ self.assertIs(S1(), S1())
109
+
110
+ class S2(Singleton):
111
+ pass
112
+
113
+ self.assertIsNot(S1(), S2())
114
+
115
+ def test_subclass(self):
116
+ class S3(Singleton):
117
+ pass
118
+
119
+ with self.assertRaises(NotImplementedError):
120
+
121
+ class S4(S3):
122
+ pass
123
+
124
+
125
+ if __name__ == "__main__":
126
+ unittest.main()
@@ -0,0 +1,5 @@
1
+ __version__ = "0.0.2"
2
+
3
+ from .singleton import SingletonType, Singleton
4
+ from .enum import EnumType, Enum
5
+ from . import log
@@ -0,0 +1,172 @@
1
+ from enum import Enum, unique, auto
2
+ from functools import cache
3
+ from typing import Self
4
+ import re
5
+
6
+
7
+ MODIFIERS = ("bold", "dim", "italic", "underline", "blinking", "inverse", "hidden", "strikethrough")
8
+
9
+ _PREFIX = "\033["
10
+
11
+
12
+ @unique
13
+ class PureColor(Enum):
14
+ """
15
+ An enum of ansi color values
16
+ """
17
+
18
+ black = 30
19
+ red = auto()
20
+ green = auto()
21
+ yellow = auto()
22
+ blue = auto()
23
+ magenta = auto()
24
+ cyan = auto()
25
+ white = auto()
26
+ default = 39
27
+
28
+
29
+ class RawColor:
30
+ """
31
+ A class that represents a color with all possible modifiers
32
+ """
33
+
34
+ __slots__ = ("color", "bright", "background", "value")
35
+
36
+ def __init__(self, color: PureColor | str | int, *, bright: bool = False, background: bool = False):
37
+ self.color = PureColor(getattr(PureColor, color) if isinstance(color, str) else color)
38
+ self.bright = bright
39
+ self.background = background
40
+ self.value = self.color.value + (10 if self.background else 0) + (60 if self.bright else 0)
41
+
42
+
43
+ class _ColorMeta(type):
44
+ """
45
+ A metaclass that implements __getattr__ at the class level for Color
46
+ """
47
+
48
+ def __getattr__(cls, item: str) -> "Color":
49
+ try:
50
+ return cls(code=_parse_color(item))
51
+ except ValueError as e:
52
+ raise AttributeError(str(e)) from e
53
+
54
+
55
+ class Color(metaclass=_ColorMeta):
56
+ """
57
+ Represents an Ansi color code; calling this on a string will apply the code to the string
58
+ Can be constructed via .factory; alternatively calling Color.<name> or Color(<name>) will
59
+ automatically construct a color. Colors are constructed with attributes determined by
60
+ splitting <name> on "_". Colors may be made bright by prepending "bright_" to the color name
61
+ Background colors may be specified by prepending "BG_" to a color name
62
+ BG_ must precede bright_ if both are specified
63
+ Foreground and background colors may both be specified at once
64
+ Examples:
65
+ Color.red
66
+ Color.default
67
+ Color.BRIGHT_BLUE
68
+ Color.BG_green
69
+ Color.Bright_BG_black
70
+ Color.bold_underline_yellow
71
+ Color.bold_bright_bg_BLUE_bright_green
72
+ Color.BOLD_bright_Red_strikethrough_bg_Bright_GREEN
73
+ """
74
+
75
+ __slots__ = ("code",)
76
+
77
+ RESET: str = f"{_PREFIX}0m"
78
+
79
+ def __init__(self, fmt: Self | RawColor | str = "", *, code: str = "") -> None:
80
+ """
81
+ Create a Color based on either the format or code, exactly one must be passed
82
+ :param fmt: A Color, RawColor, or string representation of this desired color
83
+ :param code: The ansi color code this object represents
84
+ """
85
+ if fmt != "" and code != "":
86
+ raise ValueError("code and fmt may not both be passed")
87
+ if code:
88
+ if not code.startswith(_PREFIX) or not code.endswith("m"):
89
+ raise ValueError("Invalid ansi color code")
90
+ self.code: str = code
91
+ elif isinstance(fmt, RawColor):
92
+ self.code = f"{_PREFIX}{fmt.value}m"
93
+ else:
94
+ self.code = _parse_color(fmt) if isinstance(fmt, str) else fmt.code
95
+
96
+ def __call__(self, string: str) -> str:
97
+ """
98
+ :return: The string color codes with the given ansi code
99
+ """
100
+ return f"{self.code}{string}{self.RESET}"
101
+
102
+ def __add__(self, other: Self) -> Self:
103
+ """
104
+ :return: A new color made by concatenating these two
105
+ """
106
+ if not isinstance(other, type(self)):
107
+ raise TypeError("Cannot add non-Color to Color")
108
+ return type(self)(code=f"{self.code[:-1]};{other.code[len(_PREFIX):]}")
109
+
110
+ def __repr__(self) -> str:
111
+ """
112
+ :return: The ansi color code this object represents
113
+ """
114
+ return f"<Color {repr(self.code)}>"
115
+
116
+ @classmethod
117
+ def factory(
118
+ cls, *, foreground: RawColor | None = None, background: RawColor | None = None, **modifiers
119
+ ) -> Self:
120
+ """
121
+ :return: The Ansi given the chosen color and various modifiers
122
+ """
123
+ return cls(code=_generate_code(foreground, background, modifiers))
124
+
125
+
126
+ def _generate_code(
127
+ foreground: RawColor | None, background: RawColor | None, modifiers: dict[str, bool]
128
+ ) -> str:
129
+ """
130
+ :return: The Ansi given the chosen color and various modifiers
131
+ """
132
+ if any(bad := [i for i in modifiers if i not in MODIFIERS]):
133
+ raise ValueError(f"Unknown modifier(s): {bad}")
134
+ modifiers = {i: modifiers.get(i, False) for i in MODIFIERS}
135
+ raw = (MODIFIERS.index(i) + 1 for i, k in modifiers.items() if k)
136
+ ints = [i + (0 if i < 6 else 1) for i in raw] # Ansi skips 6 for some reason
137
+ if foreground:
138
+ ints.append(foreground.value)
139
+ if background:
140
+ ints.append(background.value)
141
+ return f"{_PREFIX}{';'.join(str(i) for i in ints)}m"
142
+
143
+
144
+ @cache
145
+ def _parse_color(item: str) -> str:
146
+ """
147
+ Construct a color following the rules defined by Color
148
+ This method is not a classmethod since python3.13 deprecates mixing classmethod and cache
149
+ """
150
+ item = item.lower()
151
+ if "bright_bg" in item:
152
+ raise ValueError("bg_ must precede bright_ in color specification")
153
+ pat = f"(:?bright_)?({'|'.join(list(PureColor.__members__))})"
154
+ shx = re.findall(f"(bg_){pat}", item)
155
+ if len(shx) > 1:
156
+ raise ValueError(f"Multiple background colors specified in {item}")
157
+ bg: RawColor | None = None
158
+ if len(shx) == 1:
159
+ item = item.replace("".join(shx[0]), "")
160
+ bg = RawColor(shx[0][-1], bright="bright_" in shx[0], background=True)
161
+ shx = re.findall(pat, item)
162
+ if len(shx) > 1:
163
+ raise ValueError(f"Multiple foreground colors specified in {item}")
164
+ fg: RawColor | None = None
165
+ if len(shx) == 1:
166
+ item = item.replace("".join(shx[0]), "")
167
+ fg = RawColor(shx[0][-1], bright="bright_" in shx[0])
168
+ # Determine modifiers and construct the color
169
+ attrs = [i for i in item.split("_") if i]
170
+ if any(bad := [i for i in attrs if i not in MODIFIERS]):
171
+ raise ValueError(f"Unknown modifiers(s): {', '.join(bad)}")
172
+ return _generate_code(fg, bg, {i: True for i in MODIFIERS if i in attrs})
@@ -0,0 +1,57 @@
1
+ import collections
2
+
3
+
4
+ def _not_implemented(*_, **__):
5
+ raise NotImplementedError("Cannot instantiate this class")
6
+
7
+
8
+ class EnumType(type):
9
+ """
10
+ Metaclass for uninstantiable Enum classes with required annotations and unique values
11
+ Enum values may not be prefixed with _
12
+ These 'Enum' classes may not be modified after creation
13
+ """
14
+
15
+ def __new__(mcs, name, bases, attrs, **kwargs):
16
+ # Disallow instantiation
17
+ for bad in ("__init__", "__new__"):
18
+ if bad in attrs:
19
+ raise AttributeError("Cannot define __init__ or __new__")
20
+ attrs[bad] = _not_implemented
21
+ # Check annotations
22
+ public = {i: k for i, k in attrs.items() if not i.startswith("__")}
23
+ annotations = attrs.get("__annotations__", {})
24
+ eok = kwargs.pop("empty_ok", False)
25
+ if not public and not annotations:
26
+ if not eok:
27
+ raise ValueError("Enum type is empty")
28
+ if bad := (pub_set := set(public)) - (an_set := set(annotations)):
29
+ raise ValueError(f"All enum entries must be type annotated: {bad}")
30
+ if bad := an_set - pub_set:
31
+ raise ValueError(f"All type annotated entries must have a value: {bad}")
32
+ for i, typ in annotations.items():
33
+ if not isinstance(attrs[i], typ):
34
+ raise TypeError(f"{i} is not of type {typ}")
35
+ # Disallow duplicate values
36
+ counts = collections.Counter(public.values())
37
+ if dups := {i: k for i, k in public.items() if counts[k] > 1}:
38
+ raise ValueError(f"Duplicate values: {dups}")
39
+ # Construct class
40
+ return type.__new__(mcs, name, bases, attrs, **kwargs)
41
+
42
+ # Disallow modification
43
+
44
+ def __delattr__(cls, *_):
45
+ raise AttributeError("This class cannot be modified")
46
+
47
+ def __setattr__(cls, *_):
48
+ raise AttributeError("This class cannot be modified")
49
+
50
+
51
+ class Enum(metaclass=EnumType, empty_ok=True):
52
+ """
53
+ A uninstantiable Enum base type
54
+ Subclasses must provide type-annotated fields with unique values
55
+ Fields may not be prefixed with "_"
56
+ Derived classes will not be modifiable
57
+ """
@@ -0,0 +1,2 @@
1
+ from .cute import CuteFormatter
2
+ from . import trace
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+ from logging import CRITICAL, ERROR, WARNING, INFO, DEBUG, Formatter
3
+ from traceback import format_exception
4
+ from typing import TYPE_CHECKING
5
+ from zlib import adler32
6
+ from copy import copy
7
+
8
+ from ..ansi import PureColor, RawColor, Color
9
+
10
+ if TYPE_CHECKING:
11
+ from logging import LogRecord
12
+
13
+
14
+ class CuteFormatter(Formatter):
15
+ """
16
+ A log formatter that can print log messages with colors.
17
+ """
18
+
19
+ __slots__ = ("colored", "_color", "_cmap", "_name_width")
20
+
21
+ def __init__(
22
+ self,
23
+ colors: dict[str, Color] | None = None,
24
+ *,
25
+ colored: bool = True,
26
+ name_width: int = 12,
27
+ fmt="%(cute_levelname)s | %(cute_time)s | %(cute_name)s | %(cute_message)s%(cute_exc)s",
28
+ **kwargs,
29
+ ):
30
+ """
31
+ log_colors may be overridden, for example, trace logs are printed dimly
32
+ Do not modify cute_ parameters in the fmt string; they might have special formatting that is not visible
33
+ Ex. %(cute_name)-8s should be done via name_width, that takes into account 0-width color codes
34
+ :param colors: The colors to use for the given loggers, automatic for non-specified loggers
35
+ :param colored: If False, no colors will be used regardless of the colors parameter
36
+ :param fmt: The format string to use for the log message; do not modify cute_ parameters
37
+ :param name_width: How wide the column containing the logger name should be (minus padding)
38
+ :param kwargs: Passed to logging.Formatter
39
+ """
40
+ super().__init__(fmt=fmt, **kwargs)
41
+ self._cmap: dict[str, Color] = {}
42
+ if colors is not None:
43
+ self.update(colors)
44
+ self._name_width = name_width
45
+ self.colored: bool = colored
46
+
47
+ def update(self, colors: dict[str, Color]):
48
+ """
49
+ Set the colors for all loggers; enables colors if disabled
50
+ """
51
+ self._cmap.update(colors)
52
+
53
+ def format(self, record: LogRecord) -> str:
54
+ level: str = record.levelname.ljust(8)
55
+ when = self.formatTime(record, self.datefmt).ljust(23)
56
+ name: str = record.name.ljust(self._name_width)
57
+ message: str = record.getMessage()
58
+ if self.colored:
59
+ # Color level
60
+ if record.levelno >= CRITICAL:
61
+ level = Color.bright_red_bg_yellow(level)
62
+ elif record.levelno >= ERROR:
63
+ level = Color.red(level)
64
+ elif record.levelno >= WARNING:
65
+ level = Color.yellow(level)
66
+ elif record.levelno >= INFO:
67
+ level = Color.blue(level)
68
+ elif record.levelno < DEBUG:
69
+ level = Color.dim(level)
70
+ # Color text
71
+ if (col := self._cmap.get(record.name, None)) is None:
72
+ c: int = adler32(record.name.encode()) % 7
73
+ col = Color(RawColor(PureColor.black.value + c)) if c != 0 else Color.default
74
+ if record.levelno < DEBUG:
75
+ col += Color.dim
76
+ message = col(message)
77
+ name = col(name)
78
+ # Color timestamp
79
+ if record.levelno < DEBUG:
80
+ when = Color.dim(when)
81
+ # Create an updated record then format that
82
+ new = copy(record)
83
+ new.__dict__.update(
84
+ {
85
+ "cute_levelname": level,
86
+ "cute_time": when,
87
+ "cute_name": name,
88
+ "cute_message": message,
89
+ "cute_exc": ("\n" + "".join(format_exception(*new.exc_info))[:-1]) if new.exc_info else "",
90
+ }
91
+ )
92
+ return super().format(new)
@@ -0,0 +1,84 @@
1
+ from threading import Lock
2
+ import logging
3
+
4
+
5
+ class _State:
6
+ start: bool = False
7
+ ready: bool = False
8
+ lock = Lock()
9
+
10
+ def __init__(self):
11
+ raise NotImplementedError()
12
+
13
+
14
+ def install(*, value=logging.DEBUG // 2, force: bool = False) -> None:
15
+ """
16
+ Install TRACE, logging.trace, getLogger().trace, etc into the logging module
17
+ :param value: The TRACE log level; default: logging.DEBUG // 2
18
+ :param force: If true, force install most errors
19
+ """
20
+ with _State.lock:
21
+ _install(value, force)
22
+
23
+
24
+ # Helpers
25
+
26
+
27
+ def _define_logger_trace(lc: type, method: str, value: int) -> None:
28
+ # Define the function in the logger class, match name and help method style
29
+ def trace(self, msg, *args, **kwargs) -> None:
30
+ """
31
+ Log 'msg % args' with severity 'TRACE'.
32
+
33
+ To pass exception information, use the keyword argument exc_info with
34
+ a true TRACE, e.g.
35
+
36
+ logger.trace("Houston, we have a %s", "tiny problem", exc_info=True)
37
+ """
38
+ self.log(value, msg, *args, **kwargs)
39
+
40
+ setattr(lc, method, trace)
41
+
42
+
43
+ def _define_logging_trace(method: str, value: int) -> None:
44
+ # Define the function in the logging module, match name and help method style
45
+ def trace(msg, *args, **kwargs):
46
+ """
47
+ Log a message with severity 'TRACE' on the root logger. If the logger
48
+ has no handlers, call basicConfig() to add a console handler with a
49
+ pre-defined format.
50
+ """
51
+ logging.log(value, msg, *args, **kwargs)
52
+
53
+ setattr(logging, method, trace)
54
+
55
+
56
+ def _error_check(value: int, lgc: type, name: str, method: str) -> None:
57
+ if not 0 < value < logging.DEBUG:
58
+ raise ValueError(f"value should be within: 0 < value < {logging.DEBUG}")
59
+ if _State.ready:
60
+ raise RuntimeError("Already installed trace")
61
+ if _State.start:
62
+ raise RuntimeError("Incomplete install detected")
63
+ if hasattr(logging, name):
64
+ raise AttributeError(f"logging module already has {name} defined")
65
+ if hasattr(logging, method):
66
+ raise AttributeError(f"logging module already has {method} defined")
67
+ if hasattr(lgc, method):
68
+ raise AttributeError(f"logging.getLoggerClass() class already has {method} defined")
69
+
70
+
71
+ def _install(value: int, force: bool) -> None:
72
+ lgc = logging.getLoggerClass()
73
+ method = "trace"
74
+ name = method.upper()
75
+ # Error check
76
+ if not force:
77
+ _error_check(value, lgc, name, method)
78
+ _State.start = True
79
+ # Add trace into the logging moule
80
+ logging.addLevelName(value, name)
81
+ setattr(logging, name, value)
82
+ _define_logging_trace(method, value)
83
+ _define_logger_trace(lgc, method, value)
84
+ _State.ready = True
@@ -0,0 +1,60 @@
1
+ from threading import Condition, RLock
2
+ from typing import Any
3
+
4
+
5
+ class SingletonType(type):
6
+ """
7
+ A thread-safe singleton metaclass
8
+ """
9
+
10
+ _seen: set[type] = (
11
+ set()
12
+ ) # Objects in this have been seen by singleton before and should not be constructed again
13
+ _instances: dict[type, Any] = {} # Fully constructed singleton objects
14
+ _lock = Condition()
15
+ # For preventing subclassing of Singletons
16
+ _types: list[type] = []
17
+ _types_lock = RLock()
18
+
19
+ def __new__(mcs, name, bases, attrs, **kwargs):
20
+ """
21
+ Intercept all class definitions to prevent subclassing singleton types except for Singleton
22
+ """
23
+ if "__init_subclass__" in attrs:
24
+ raise NotImplementedError("Singleton's should not be subclassed")
25
+
26
+ def _init_subclass(cls):
27
+ with mcs._types_lock:
28
+ if any(issubclass(cls, i) for i in mcs._types if i is not Singleton):
29
+ err = "Do not derive from Singletons other than the base Singleton type"
30
+ raise NotImplementedError(err)
31
+ mcs._types.append(cls)
32
+
33
+ attrs["__init_subclass__"] = _init_subclass
34
+ ret = super().__new__(mcs, name, bases, attrs, **kwargs)
35
+ with mcs._types_lock:
36
+ mcs._types.append(ret)
37
+ return ret
38
+
39
+ def __call__(cls, *args, **kwargs):
40
+ """
41
+ Intercept all instantiations to ensure at most one instance exists
42
+ """
43
+ # If the object has been seen before, wait for it to be available then return it
44
+ with cls._lock:
45
+ if cls in cls._seen:
46
+ cls._lock.wait_for(lambda: cls in cls._instances)
47
+ return cls._instances[cls]
48
+ cls._seen.add(cls)
49
+ # The object has not been constructed before, create it outside of any lock to avoid delays
50
+ obj = super().__call__(*args, **kwargs)
51
+ with cls._lock:
52
+ cls._instances[cls] = obj
53
+ cls._lock.notify_all() # Notify other threads of the new object
54
+ return obj
55
+
56
+
57
+ class Singleton(metaclass=SingletonType):
58
+ """
59
+ A thread-safe singleton base class
60
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zstdlib
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: A set of useful python utilities
5
5
  License: GPLv3
6
6
  Project-URL: Homepage, https://github.com/zwimer/zstdlib
@@ -12,3 +12,6 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
+
16
+ # zstdlib
17
+ A set of useful python utilities
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tests/__init__.py
5
+ tests/test_ansi.py
6
+ tests/test_enum.py
7
+ tests/test_singleton.py
8
+ tests/log/__init__.py
9
+ tests/log/base.py
10
+ tests/log/test_cute.py
11
+ tests/log/test_trace.py
12
+ zstdlib/__init__.py
13
+ zstdlib/ansi.py
14
+ zstdlib/enum.py
15
+ zstdlib/py.typed
16
+ zstdlib/singleton.py
17
+ zstdlib.egg-info/PKG-INFO
18
+ zstdlib.egg-info/SOURCES.txt
19
+ zstdlib.egg-info/dependency_links.txt
20
+ zstdlib.egg-info/top_level.txt
21
+ zstdlib/log/__init__.py
22
+ zstdlib/log/cute.py
23
+ zstdlib/log/trace.py
@@ -1 +1,2 @@
1
+ tests
1
2
  zstdlib
@@ -1,5 +0,0 @@
1
- __version__ = "0.0.1"
2
-
3
- from .singleton import Singleton
4
- from .enum import UEnum
5
- from . import trace
@@ -1,32 +0,0 @@
1
- # pylint: disable=bad-mcs-method-argument,bad-mcs-classmethod-argument
2
- class UEnum(type):
3
- """
4
- Metaclass for non-instantiable Enum classes with unique values
5
- These 'Enum' classes may not be modified after creation
6
- """
7
-
8
- def __new__(mcls, name, bases, attrs, **kwargs):
9
- def _ni(*_, **__):
10
- raise NotImplementedError("Cannot instantiate this class")
11
-
12
- # Disallow instantiation
13
- for bad in ("__init__", "__new__"):
14
- if bad in attrs:
15
- raise ValueError("Cannot define __init__ or __new__")
16
- attrs[bad] = _ni
17
- # Disallow duplicate values
18
- values = set()
19
- for v in (k for i, k in attrs.items() if not i.startswith("__")):
20
- if v in values:
21
- raise ValueError(f"Duplicate value: {v}")
22
- values.add(v)
23
- # Construct class
24
- return type.__new__(mcls, name, bases, attrs, **kwargs)
25
-
26
- # Disallow modification
27
-
28
- def __delattr__(self, *_):
29
- raise AttributeError("This class cannot be modified")
30
-
31
- def __setattr__(self, *_):
32
- raise AttributeError("This class cannot be modified")
@@ -1,28 +0,0 @@
1
- from threading import Condition
2
- from typing import Any
3
-
4
-
5
- class Singleton(type):
6
- """
7
- A thread-safe singleton metaclass
8
- """
9
-
10
- _seen: set[type] = (
11
- set()
12
- ) # Objects in this have been seen by singleton before and should not be constructed again
13
- _instances: dict[type, Any] = {} # Fully constructed singleton objects
14
- _lock = Condition()
15
-
16
- def __call__(cls, *args, **kwargs):
17
- # If the object has been seen before, wait for it to be available then return it
18
- with cls._lock:
19
- if cls in cls._seen:
20
- cls._lock.wait_for(lambda: cls in cls._instances)
21
- return cls._instances[cls]
22
- cls._seen.add(cls)
23
- # The object has not been constructed before, create it outside of any lock to avoid delays
24
- obj = super().__call__(*args, **kwargs)
25
- with cls._lock:
26
- cls._instances[cls] = obj
27
- cls._lock.notify_all() # Notify other threads of the new object
28
- return obj
@@ -1,55 +0,0 @@
1
- import logging
2
-
3
-
4
- def _define_logger_trace(LC: type, method: str, TRACE: int):
5
- # Define the function in the logger class, match name and help method style
6
- def trace(self, msg, *args, **kwargs) -> None:
7
- """
8
- Log 'msg % args' with severity 'TRACE'.
9
-
10
- To pass exception information, use the keyword argument exc_info with
11
- a true TRACE, e.g.
12
-
13
- logger.trace("Houston, we have a %s", "tiny problem", exc_info=True)
14
- """
15
- self.log(TRACE, msg, *args, **kwargs)
16
-
17
- setattr(LC, method, trace)
18
-
19
-
20
- def _define_logging_trace(method: str, TRACE: int):
21
- # Define the function in the logging module, match name and help method style
22
- def trace(msg, *args, **kwargs):
23
- """
24
- Log a message with severity 'TRACE' on the root logger. If the logger
25
- has no handlers, call basicConfig() to add a console handler with a
26
- pre-defined format.
27
- """
28
- logging.log(TRACE, msg, *args, **kwargs)
29
-
30
- setattr(logging, method, trace)
31
-
32
-
33
- def install() -> None:
34
- """
35
- Install TRACE, logging.trace, getLogger().trace, etc into the logging module
36
- """
37
- if logging.DEBUG < 2:
38
- raise ValueError("logging.DEBUG is too small")
39
- TRACE: int = logging.DEBUG // 2
40
- name = "TRACE"
41
- method = name.lower()
42
-
43
- # Error check
44
- if hasattr(logging, name):
45
- raise AttributeError(f"logging module already has {name} defined")
46
- if hasattr(logging, method):
47
- raise AttributeError(f"logging module already has {method} defined")
48
- if hasattr(LC := logging.getLoggerClass(), method):
49
- raise AttributeError(f"logging.getLoggerClass() class already has {method} defined")
50
-
51
- # Add trace into the logging moule
52
- logging.addLevelName(TRACE, name)
53
- setattr(logging, name, TRACE)
54
- _define_logger_trace(LC, method, TRACE)
55
- _define_logging_trace(method, TRACE)
@@ -1,11 +0,0 @@
1
- LICENSE
2
- pyproject.toml
3
- zstdlib/__init__.py
4
- zstdlib/enum.py
5
- zstdlib/py.typed
6
- zstdlib/singleton.py
7
- zstdlib/trace.py
8
- zstdlib.egg-info/PKG-INFO
9
- zstdlib.egg-info/SOURCES.txt
10
- zstdlib.egg-info/dependency_links.txt
11
- zstdlib.egg-info/top_level.txt
File without changes
File without changes
File without changes