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.
- {zstdlib-0.0.1/zstdlib.egg-info → zstdlib-0.0.2}/PKG-INFO +4 -1
- zstdlib-0.0.2/README.md +2 -0
- {zstdlib-0.0.1 → zstdlib-0.0.2}/pyproject.toml +8 -3
- zstdlib-0.0.2/tests/__init__.py +0 -0
- zstdlib-0.0.2/tests/log/__init__.py +0 -0
- zstdlib-0.0.2/tests/log/base.py +69 -0
- zstdlib-0.0.2/tests/log/test_cute.py +98 -0
- zstdlib-0.0.2/tests/log/test_trace.py +74 -0
- zstdlib-0.0.2/tests/test_ansi.py +56 -0
- zstdlib-0.0.2/tests/test_enum.py +137 -0
- zstdlib-0.0.2/tests/test_singleton.py +126 -0
- zstdlib-0.0.2/zstdlib/__init__.py +5 -0
- zstdlib-0.0.2/zstdlib/ansi.py +172 -0
- zstdlib-0.0.2/zstdlib/enum.py +57 -0
- zstdlib-0.0.2/zstdlib/log/__init__.py +2 -0
- zstdlib-0.0.2/zstdlib/log/cute.py +92 -0
- zstdlib-0.0.2/zstdlib/log/trace.py +84 -0
- zstdlib-0.0.2/zstdlib/singleton.py +60 -0
- {zstdlib-0.0.1 → zstdlib-0.0.2/zstdlib.egg-info}/PKG-INFO +4 -1
- zstdlib-0.0.2/zstdlib.egg-info/SOURCES.txt +23 -0
- {zstdlib-0.0.1 → zstdlib-0.0.2}/zstdlib.egg-info/top_level.txt +1 -0
- zstdlib-0.0.1/zstdlib/__init__.py +0 -5
- zstdlib-0.0.1/zstdlib/enum.py +0 -32
- zstdlib-0.0.1/zstdlib/singleton.py +0 -28
- zstdlib-0.0.1/zstdlib/trace.py +0 -55
- zstdlib-0.0.1/zstdlib.egg-info/SOURCES.txt +0 -11
- {zstdlib-0.0.1 → zstdlib-0.0.2}/LICENSE +0 -0
- {zstdlib-0.0.1 → zstdlib-0.0.2}/setup.cfg +0 -0
- {zstdlib-0.0.1 → zstdlib-0.0.2}/zstdlib/py.typed +0 -0
- {zstdlib-0.0.1 → zstdlib-0.0.2}/zstdlib.egg-info/dependency_links.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: zstdlib
|
|
3
|
-
Version: 0.0.
|
|
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
|
zstdlib-0.0.2/README.md
ADDED
|
@@ -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
|
-
"
|
|
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 = [
|
|
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,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,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.
|
|
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
|
zstdlib-0.0.1/zstdlib/enum.py
DELETED
|
@@ -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
|
zstdlib-0.0.1/zstdlib/trace.py
DELETED
|
@@ -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
|
|
File without changes
|