zstdlib 0.0.7__tar.gz → 0.0.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {zstdlib-0.0.7 → zstdlib-0.0.11}/PKG-INFO +3 -3
  2. {zstdlib-0.0.7 → zstdlib-0.0.11}/pyproject.toml +3 -3
  3. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/log/test_cute.py +22 -0
  4. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/test_enum.py +4 -4
  5. zstdlib-0.0.11/tests/test_io.py +90 -0
  6. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/test_singleton.py +2 -2
  7. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/__init__.py +2 -2
  8. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/ansi.py +6 -0
  9. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/enum.py +1 -5
  10. zstdlib-0.0.11/zstdlib/io.py +238 -0
  11. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/log/cute.py +33 -25
  12. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/log/trace.py +1 -3
  13. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/singleton.py +2 -2
  14. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib.egg-info/PKG-INFO +3 -3
  15. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib.egg-info/SOURCES.txt +2 -0
  16. {zstdlib-0.0.7 → zstdlib-0.0.11}/LICENSE +0 -0
  17. {zstdlib-0.0.7 → zstdlib-0.0.11}/README.md +0 -0
  18. {zstdlib-0.0.7 → zstdlib-0.0.11}/setup.cfg +0 -0
  19. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/__init__.py +0 -0
  20. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/log/__init__.py +0 -0
  21. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/log/base.py +0 -0
  22. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/log/test_trace.py +0 -0
  23. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/not_set.py +0 -0
  24. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/test_ansi.py +0 -0
  25. {zstdlib-0.0.7 → zstdlib-0.0.11}/tests/test_frozen.py +0 -0
  26. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/frozen.py +0 -0
  27. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/log/__init__.py +0 -0
  28. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/not_set.py +0 -0
  29. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib/py.typed +0 -0
  30. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib.egg-info/dependency_links.txt +0 -0
  31. {zstdlib-0.0.7 → zstdlib-0.0.11}/zstdlib.egg-info/top_level.txt +0 -0
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zstdlib
3
- Version: 0.0.7
3
+ Version: 0.0.11
4
4
  Summary: A set of useful python utilities
5
5
  License: GPLv3
6
6
  Project-URL: Homepage, https://github.com/zwimer/zstdlib
7
7
  Keywords: remote,pipe
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
12
- Requires-Python: >=3.10
12
+ Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
 
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
  name = "zstdlib"
7
7
  classifiers = [
8
8
  "Programming Language :: Python :: 3",
9
- "Programming Language :: Python :: 3.10",
9
+ "Programming Language :: Python :: 3.12",
10
10
  "Development Status :: 5 - Production/Stable",
11
11
  "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
12
12
  ]
@@ -14,7 +14,7 @@ keywords = ["remote", "pipe"]
14
14
  license = {text = "GPLv3"}
15
15
  description = "A set of useful python utilities"
16
16
  urls = {Homepage = "https://github.com/zwimer/zstdlib"}
17
- requires-python = ">= 3.10"
17
+ requires-python = ">= 3.12"
18
18
  # dependencies = [
19
19
  # # Client
20
20
  # "zstandard[cffi]",
@@ -63,7 +63,7 @@ disable = [
63
63
 
64
64
  [tool.black]
65
65
  line-length = 110
66
- target-version = ["py310", "py311", "py312", "py313"]
66
+ target-version = ["py312", "py313"]
67
67
 
68
68
  [tool.ruff]
69
69
  line-length = 110
@@ -78,6 +78,28 @@ class TestCuteFormatter(LeftBase, unittest.TestCase):
78
78
  self.assertEqual(Color.bright_red_bg_yellow("CRITICAL".ljust(8)), l_name(5))
79
79
  self.assertEqual(Color.bright_red_bg_yellow("Level 60".ljust(8)), l_name(6))
80
80
 
81
+ def test_custom_dim_level(self) -> None:
82
+ name = "TestCuteFormatter.test_dim_level"
83
+ msg = "test"
84
+ with self.hijack(name, fmt=CuteFormatter(dim_level=5)) as log:
85
+ log.log(4, msg)
86
+ log.log(5, msg)
87
+ self.assertEqual(2, len(self.messages[log]))
88
+ l_name = lambda idx: self.messages[log][idx].split("|")[0].strip()
89
+ self.assertEqual(Color.dim("Level 4".ljust(8)), l_name(0))
90
+ self.assertEqual("Level 5", l_name(1))
91
+
92
+ def test_custom_level_color(self) -> None:
93
+ name = "TestCuteFormatter.test_custom_level_color"
94
+ msg = "test"
95
+ with self.hijack(name, fmt=CuteFormatter(level_colors={15: Color.bright_red_bg_yellow})) as log:
96
+ log.log(14, msg)
97
+ log.log(15, msg)
98
+ self.assertEqual(2, len(self.messages[log]))
99
+ l_name = lambda idx: self.messages[log][idx].split("|")[0].strip()
100
+ self.assertEqual("Level 14".ljust(8), l_name(0))
101
+ self.assertEqual(Color.bright_red_bg_yellow("Level 15".ljust(8)), l_name(1))
102
+
81
103
  def test_exception(self) -> None:
82
104
  name = "TestCuteFormatter.test_exception"
83
105
  with self.hijack(name, fmt=CuteFormatter()) as log:
@@ -63,9 +63,9 @@ class TestEnumType(unittest.TestCase):
63
63
  class ET10(metaclass=EnumType):
64
64
  arg1: int = 0
65
65
 
66
- with self.assertRaises(NotImplementedError):
66
+ with self.assertRaises(TypeError):
67
67
  ET10()
68
- with self.assertRaises(NotImplementedError):
68
+ with self.assertRaises(TypeError):
69
69
  ET10.__init__({})
70
70
 
71
71
 
@@ -127,9 +127,9 @@ class TestEnum(unittest.TestCase):
127
127
  class E10(Enum):
128
128
  arg1: int = 0
129
129
 
130
- with self.assertRaises(NotImplementedError):
130
+ with self.assertRaises(TypeError):
131
131
  E10()
132
- with self.assertRaises(NotImplementedError):
132
+ with self.assertRaises(TypeError):
133
133
  E10.__init__({})
134
134
 
135
135
 
@@ -0,0 +1,90 @@
1
+ import tempfile
2
+ import unittest
3
+
4
+ from zstdlib.io import ProtectedFile, BinaryIO, TextIO, io
5
+
6
+
7
+ class RamFile(tempfile.SpooledTemporaryFile):
8
+ """
9
+ A file-like object that stores its contents in memory.
10
+ """
11
+
12
+ rollover = None # type: ignore
13
+
14
+ def __init__(self, *args, mode: str = "w+", **kwargs):
15
+ super().__init__(10**9, *args, mode=mode, **kwargs)
16
+
17
+ def load(self, x):
18
+ self.write(x)
19
+ self.flush()
20
+ self.seek(self.tell() - len(x))
21
+
22
+
23
+ class TestIO(unittest.TestCase):
24
+
25
+ def test_init(self):
26
+ with RamFile(mode="wb+") as fb, RamFile() as fs:
27
+ ts = TextIO(fs)
28
+ tb = BinaryIO(fb)
29
+ with self.assertRaises(TypeError):
30
+ TextIO(fb)
31
+ with self.assertRaises(TypeError):
32
+ BinaryIO(fs)
33
+ self.assertIs(ts, io(fs))
34
+ self.assertIs(tb, io(fb))
35
+
36
+ def test_protected_file(self):
37
+ with RamFile() as f:
38
+ p = ProtectedFile(f)
39
+ self.assertIs(f, p.raw)
40
+ self.assertIs(f.mode, p.mode)
41
+ for i in ("read", "readline", "readlines", "readall", "seek"):
42
+ with self.assertRaises(AttributeError):
43
+ getattr(p, i)
44
+
45
+ def test_unread(self):
46
+ with RamFile() as raw:
47
+ f = io(raw)
48
+ f.unread("hello")
49
+ self.assertEqual("hello", f._buffer)
50
+
51
+ def test_read(self):
52
+ with RamFile() as raw:
53
+ raw.load("foobaz")
54
+ f = io(raw)
55
+ # Test read of file
56
+ self.assertEqual("fo", f.read(2))
57
+ self.assertEqual("o", f.read(1))
58
+ # Test read of buffer
59
+ f.unread("bar")
60
+ self.assertEqual("b", f.read(1))
61
+ # Test read of file + buffer
62
+ f.unread("foob")
63
+ self.assertEqual("foobar", f.read(6))
64
+ f.unread("foobar")
65
+ self.assertEqual("foobarbaz", f.read())
66
+
67
+ def test_peek(self):
68
+ with RamFile() as raw:
69
+ f = io(raw)
70
+ f.unread("foo")
71
+ # Test peek of buffer
72
+ for i in range(3):
73
+ self.assertEqual("fo", f.peek(2))
74
+ self.assertEqual("foo", f.read())
75
+ # Test peek of file
76
+ raw.load("bar")
77
+ for i in range(3):
78
+ self.assertEqual("ba", f.peek(2))
79
+ self.assertEqual("bar", f.read())
80
+ # Test peek of both
81
+ f.unread("baz")
82
+ raw.load("qux")
83
+ for i in range(3):
84
+ for k in range(5):
85
+ self.assertEqual("bazqu"[:k], f.peek(k))
86
+ self.assertEqual("bazqux", f.read())
87
+
88
+
89
+ if __name__ == "__main__":
90
+ unittest.main()
@@ -23,7 +23,7 @@ class TestSingletonType(unittest.TestCase):
23
23
  class ST3(metaclass=SingletonType):
24
24
  pass
25
25
 
26
- with self.assertRaises(NotImplementedError):
26
+ with self.assertRaises(TypeError):
27
27
 
28
28
  class ST4(ST3):
29
29
  pass
@@ -116,7 +116,7 @@ class TestSingleton(unittest.TestCase):
116
116
  class S3(Singleton):
117
117
  pass
118
118
 
119
- with self.assertRaises(NotImplementedError):
119
+ with self.assertRaises(TypeError):
120
120
 
121
121
  class S4(S3):
122
122
  pass
@@ -1,7 +1,7 @@
1
- __version__ = "0.0.7"
1
+ __version__ = "0.0.11"
2
2
 
3
3
  from .not_set import NotSetType, NotSet
4
4
  from .frozen import Freezable, frozen
5
5
  from .singleton import Singleton
6
6
  from .enum import Enum
7
- from . import log
7
+ from . import log, io
@@ -107,6 +107,12 @@ class Color(metaclass=_ColorMeta):
107
107
  raise TypeError("Cannot add non-Color to Color")
108
108
  return type(self)(code=f"{self.code[:-1]};{other.code[len(_PREFIX):]}")
109
109
 
110
+ def __eq__(self, other: object) -> bool:
111
+ """
112
+ :return: Whether the two colors are equal
113
+ """
114
+ return isinstance(other, type(self)) and self.code == other.code
115
+
110
116
  def __repr__(self) -> str:
111
117
  """
112
118
  :return: The ansi color code this object represents
@@ -1,10 +1,6 @@
1
1
  import collections
2
2
 
3
3
 
4
- def _not_implemented(*_, **__):
5
- raise NotImplementedError("Cannot instantiate this class")
6
-
7
-
8
4
  class EnumType(type):
9
5
  """
10
6
  Metaclass for uninstantiable Enum classes with required annotations and unique values
@@ -17,7 +13,7 @@ class EnumType(type):
17
13
  for bad in ("__init__", "__new__"):
18
14
  if bad in attrs:
19
15
  raise AttributeError("Cannot define __init__ or __new__")
20
- attrs[bad] = _not_implemented
16
+ attrs[bad] = None
21
17
  # Check annotations
22
18
  public = {i: k for i, k in attrs.items() if not i.startswith("__")}
23
19
  annotations = attrs.get("__annotations__", {})
@@ -0,0 +1,238 @@
1
+ from io import (
2
+ IOBase,
3
+ TextIOBase as _PyRawTextIOBase,
4
+ RawIOBase as _PyRawIOBase,
5
+ BufferedIOBase as _PyBufferedIOBase,
6
+ )
7
+ from threading import RLock
8
+ from itertools import chain
9
+ from typing import Self
10
+ import weakref
11
+
12
+
13
+ __all__ = ("TextIO", "BinaryIO", "io")
14
+
15
+ type _BinaryBase = _PyRawIOBase | _PyBufferedIOBase
16
+ type _TextBase = _PyRawTextIOBase
17
+
18
+
19
+ class ProtectedFile:
20
+ """
21
+ A wrapper around an IO object that prevents access to certain attributes
22
+ If absolutely necessary, users can access the raw object directly via .raw
23
+ """
24
+
25
+ PROTECTED_PREFIXES = ("read", "seek", "getbuffer", "getvalue", "detach")
26
+ __slots__ = ("raw",)
27
+
28
+ def __init__(self, raw: IOBase) -> None:
29
+ """
30
+ :param raw: The raw IO object to wrap, users can access it directly if they must
31
+ """
32
+ self.raw: IOBase = raw
33
+
34
+ def __getattr__(self, item):
35
+ """
36
+ Prevent access to certain attributes
37
+ """
38
+ if any(item.startswith(i) for i in self.PROTECTED_PREFIXES):
39
+ raise AttributeError(f"Attribute {item} is protected")
40
+ return getattr(self.raw, item)
41
+
42
+ def __hash__(self) -> int:
43
+ return hash(self.raw)
44
+
45
+
46
+ # See bug: https://github.com/pylint-dev/pylint/issues/9335
47
+ # pylint: disable=undefined-variable
48
+ class _IOWrapperBase[T: (str, bytes)]:
49
+ """
50
+ A base class for IO objects that adds additional functionality such as readuntil, unread, and peek
51
+ These classes are singletons with respect to their input object
52
+ Note: This class takes ownership of the input object, do not use it elsewhere
53
+ """
54
+
55
+ __slots__ = ("lock", "f", "_buffer", "_eof")
56
+ _instances: weakref.WeakKeyDictionary[IOBase, Self] = weakref.WeakKeyDictionary()
57
+ _wr_lock = RLock()
58
+
59
+ def __new__(cls, f: IOBase, binary: bool) -> Self:
60
+ """
61
+ Create a new instance of cls for raw, or return an existing instance if one exists
62
+ """
63
+ with cls._wr_lock:
64
+ if (ret := cls._instances.get(f, None)) is None:
65
+ cls._instances[f] = (ret := super().__new__(cls))
66
+ return ret
67
+
68
+ def __init__(self, f: IOBase, *, binary: bool) -> None:
69
+ """
70
+ Initialize the IO object with a raw IO object
71
+ Note: This class takes ownership of the input object, do not use it elsewhere
72
+ :param f: The raw IO object to wrap
73
+ :param binary: If True, the IO object will read/write bytes, otherwise it will read/write strings
74
+ """
75
+ self._buffer: T = (bytes if binary else str)() # type: ignore
76
+ self.f = ProtectedFile(f)
77
+ self.lock = RLock()
78
+
79
+ def read(self, size: int = -1) -> T:
80
+ """
81
+ Read up to size characters from self.raw or until EOF
82
+ If size is unspecified or -1, read until EOF
83
+ """
84
+ if size == 0:
85
+ return self._buffer[:0]
86
+ with self.lock:
87
+ ret = self._read_buffer(size)
88
+ if len(ret) == size:
89
+ return ret
90
+ ret += self._read(-1 if size == -1 else (size - len(ret)))
91
+ return ret
92
+
93
+ def readline(self) -> T:
94
+ """
95
+ Read a line from self.raw or until EOF
96
+ Newline is included in the return value
97
+ """
98
+ with self.lock:
99
+ nl: T = "\n" if isinstance(self._buffer, str) else b"\n"
100
+ ret = self._read_buffer(self._buffer.find(nl)) + self._read_buffer(1)
101
+ if ret.endswith(nl):
102
+ return ret
103
+ return ret + self.f.raw.readline() # type: ignore
104
+
105
+ def readlines(self) -> list[T]:
106
+ """
107
+ Read all lines from self.raw until EOF
108
+ """
109
+ with self.lock:
110
+ ret: list[T] = []
111
+ while add := self.readline():
112
+ ret.append(add)
113
+ return ret
114
+
115
+ def peek(self, size: int = -1) -> T:
116
+ """
117
+ Read size characters from self.raw or until EOF, without consuming the data
118
+ """
119
+ with self.lock:
120
+ self.unread(ret := self.read(size))
121
+ return ret
122
+
123
+ def unread(self, data: T) -> None:
124
+ """
125
+ Unread data back to the internal buffer
126
+ """
127
+ with self.lock:
128
+ self._buffer = data + self._buffer
129
+
130
+ def readuntil(self, until: T, *, eof_ok: bool = True) -> T:
131
+ """
132
+ Read until until is found in f. If eof_ok is False, raise EOFError if until is not found before EOF.
133
+ :param until: The str/bytes to read until
134
+ :param inclusive: If True, the returned value will end with until
135
+ :param eof_ok: if False, raise EOFError if until is not found before EOF
136
+ :return: The data read from self.raw, up to and including until
137
+ """
138
+ if isinstance(until, str) ^ isinstance(self._buffer, str):
139
+ raise TypeError("until: expected same type as buffer")
140
+ if len(until) == 0:
141
+ raise ValueError("until may not be empty")
142
+ with self.lock:
143
+ if until in self._buffer:
144
+ return self._read_buffer(self._buffer.find(until) + len(until))
145
+ buffer = [self._read_buffer()]
146
+ # Use bytearray to avoid multiple concatenations
147
+ # Read until until is found, avoid small read() syscall's because they are slow
148
+ while (n := self._remainder(buffer, until)) and (add := self._read(n)):
149
+ buffer.append(add)
150
+ ret = buffer[0][:0].join(buffer)
151
+ if not ret.endswith(until) and not eof_ok:
152
+ raise EOFError("until: not found before EOF")
153
+ return ret
154
+
155
+ #
156
+ # Helper functions
157
+ #
158
+
159
+ def _read(self, size) -> T:
160
+ ret: T = self.f.raw.read(size) # type: ignore
161
+ return ret if ret else self._buffer[:0] # Some .read's can return None
162
+
163
+ def _read_buffer(self, size: int = -1) -> T:
164
+ if size == -1:
165
+ size = len(self._buffer)
166
+ ret: T = self._buffer[:size]
167
+ self._buffer = self._buffer[size:]
168
+ return ret
169
+
170
+ @staticmethod
171
+ def _remainder(data: list[T], suffix: T) -> int:
172
+ assert suffix, "suffix: expected non-empty str or bytes"
173
+ rev = chain.from_iterable(reversed(i) for i in reversed(data))
174
+ for index, (i, k) in enumerate(zip(rev, reversed(suffix))):
175
+ if i != k:
176
+ return len(suffix) - index
177
+ return 0
178
+
179
+ #
180
+ # Protection
181
+ #
182
+
183
+ def __hash__(self) -> int:
184
+ return hash((self.__class__, self.f))
185
+
186
+ __deepcopy__ = None # type: ignore
187
+ __copy__ = None # type: ignore
188
+
189
+
190
+ def _mode(f: IOBase, binary: bool | None) -> bool:
191
+ mode = getattr(f, "mode", "")
192
+ binary = binary or "b" in mode or isinstance(f, (_PyRawIOBase, _PyBufferedIOBase))
193
+ text = (binary is False) or (mode and "b" not in mode) or isinstance(f, _PyRawTextIOBase)
194
+ if not text and not binary:
195
+ raise TypeError("Cannot determine if IO object is text or binary")
196
+ if text and binary:
197
+ raise TypeError("IO object is both text and binary")
198
+ return text
199
+
200
+
201
+ class TextIO(_IOWrapperBase[str]):
202
+ """
203
+ A wrapper around TextIO objects that add additional functionality such as readuntil, unread, and peek
204
+ """
205
+
206
+ def __new__(cls, f: IOBase):
207
+ if not _mode(f, binary=False):
208
+ raise TypeError("raw should be a non-binary IO object")
209
+ return super().__new__(cls, f, binary=False)
210
+
211
+ def __init__(self, f: IOBase):
212
+ super().__init__(f, binary=False)
213
+
214
+
215
+ class BinaryIO(_IOWrapperBase[bytes]):
216
+ """
217
+ A wrapper around BinaryIO objects that add additional functionality such as readuntil, unread, and peek
218
+ """
219
+
220
+ def __new__(cls, f: IOBase):
221
+ if _mode(f, binary=True):
222
+ raise TypeError("raw should be a binary IO object")
223
+ return super().__new__(cls, f, binary=True)
224
+
225
+ def __init__(self, f: IOBase):
226
+ super().__init__(f, binary=False)
227
+
228
+
229
+ def io(raw: IOBase, binary: bool | None = None) -> TextIO | BinaryIO:
230
+ """
231
+ Convert an IO object to a TextIO or BinaryIO object
232
+ For raw to be valid, one of the three must be true:
233
+ 1. raw is an io.TextIOBase, io.RawIOBase, or io.BufferedIOBase object
234
+ 2. raw has a .mode string attribute
235
+ 3. binary must be set
236
+ If the three detection methods disagree on if raw is binary, a TypeError is raised
237
+ """
238
+ return TextIO(raw) if _mode(raw, binary) else BinaryIO(raw)
@@ -16,17 +16,25 @@ class CuteFormatter(Formatter):
16
16
  A log formatter that can print log messages with colors.
17
17
  """
18
18
 
19
- DEFAULT_CUTE_WIDTHS = {"cute_levelname": 8, "cute_time": 23, "cute_name": 12}
20
- __slots__ = ("colored", "_color", "_cmap", "_name_width")
19
+ __slots__ = ("colored", "_color", "_cmap", "_lvl_cmap", "_dim_level", "_name_width")
20
+ DEFAULT_CUTE_WIDTHS: dict[str, int] = {"cute_levelname": 8, "cute_time": 23, "cute_name": 12}
21
+ DEFAULT_LEVEL_COLORS: dict[int, Color] = {
22
+ INFO: Color.blue,
23
+ WARNING: Color.yellow,
24
+ ERROR: Color.red,
25
+ CRITICAL: Color.bright_red_bg_yellow,
26
+ }
21
27
 
22
- # pylint: disable=dangerous-default-value
28
+ # pylint: disable=dangerous-default-value,keyword-arg-before-vararg,too-many-arguments
23
29
  def __init__(
24
30
  self,
25
- colors: dict[str, Color] | None = None,
26
- *,
31
+ fmt="%(cute_levelname)s | %(cute_asctime)s | %(cute_name)s | %(cute_message)s",
32
+ *args,
27
33
  colored: bool = True,
34
+ dim_level: int = DEBUG,
35
+ colors: dict[str, Color] = {},
36
+ level_colors: dict[int, Color] = {},
28
37
  cute_widths: dict[str, int] = {},
29
- fmt="%(cute_levelname)s | %(cute_asctime)s | %(cute_name)s | %(cute_message)s",
30
38
  **kwargs,
31
39
  ):
32
40
  """
@@ -34,16 +42,20 @@ class CuteFormatter(Formatter):
34
42
  Do not modify cute_ parameters in the fmt string; they might have special formatting that is not visible
35
43
  Ex. %(cute_name)-8s should be done via cute_widths["cute_name"] = 8
36
44
  This is because cute_widths takes into account 0-width color codes when padding
37
- :param colors: The colors to use for the given loggers, automatic for non-specified loggers
38
- :param colored: If False, no colors will be used regardless of the colors parameter
45
+ Level colors used are: DEFAULT_LEVEL_COLORS | level_colors
46
+ Dimming of log messages is done for levels < dim_level
39
47
  :param fmt: The format string to use for the log message; do not modify cute_ parameters
48
+ :param args: Passed to logging.Formatter
49
+ :param colored: If False, no colors will be used regardless of the colors parameter
50
+ :param colors: The colors to use for the given loggers, automatic for non-specified loggers
51
+ :param level_colors: The colors to use >= the given levels, automatic for non-specified loggers
40
52
  :param cute_widths: The widths of the cute_ columns in the log message
41
53
  :param kwargs: Passed to logging.Formatter
42
54
  """
43
- super().__init__(fmt=fmt, **kwargs)
44
- self._cmap: dict[str, Color] = {}
45
- if colors is not None:
46
- self.update(colors)
55
+ super().__init__(fmt, *args, **kwargs)
56
+ self._dim_level: int = dim_level
57
+ self._cmap: dict[str, Color] = dict(colors)
58
+ self._lvl_cmap: dict[int, Color] = self.DEFAULT_LEVEL_COLORS | level_colors
47
59
  self._widths = defaultdict(int, self.DEFAULT_CUTE_WIDTHS | cute_widths)
48
60
  if any(not i.startswith("cute_") for i in self._widths):
49
61
  raise ValueError("cute_widths must only contain cute_ parameters")
@@ -53,7 +65,7 @@ class CuteFormatter(Formatter):
53
65
  """
54
66
  Set the colors for all loggers; enables colors if disabled
55
67
  """
56
- self._cmap.update(colors)
68
+ self._cmap |= colors
57
69
 
58
70
  def format(self, record: LogRecord) -> str:
59
71
  levelname: str = record.levelname.ljust(self._widths["cute_levelname"])
@@ -62,26 +74,22 @@ class CuteFormatter(Formatter):
62
74
  message: str = record.getMessage().ljust(self._widths["cute_message"])
63
75
  if self.colored:
64
76
  # Color level
65
- if record.levelno >= CRITICAL:
66
- levelname = Color.bright_red_bg_yellow(levelname)
67
- elif record.levelno >= ERROR:
68
- levelname = Color.red(levelname)
69
- elif record.levelno >= WARNING:
70
- levelname = Color.yellow(levelname)
71
- elif record.levelno >= INFO:
72
- levelname = Color.blue(levelname)
73
- elif record.levelno < DEBUG:
74
- levelname = Color.dim(levelname)
77
+ opts = tuple(i for i in self._lvl_cmap if i <= record.levelno)
78
+ col: Color | None = self._lvl_cmap[max(opts)] if opts else None
79
+ if record.levelno < self._dim_level:
80
+ col = Color.dim if col is None else (col + Color.dim)
81
+ if col is not None:
82
+ levelname = col(levelname)
75
83
  # Color text
76
84
  if (col := self._cmap.get(record.name, None)) is None:
77
85
  c: int = adler32(record.name.encode()) % 7
78
86
  col = Color(RawColor(PureColor.black.value + c)) if c != 0 else Color.default
79
- if record.levelno < DEBUG:
87
+ if record.levelno < self._dim_level:
80
88
  col += Color.dim
81
89
  message = col(message)
82
90
  name = col(name)
83
91
  # Color timestamp
84
- if record.levelno < DEBUG:
92
+ if record.levelno < self._dim_level:
85
93
  asctime = Color.dim(asctime)
86
94
  # Create an updated record then format that
87
95
  new = copy(record)
@@ -3,13 +3,11 @@ import logging
3
3
 
4
4
 
5
5
  class _State:
6
+ __init__ = None # type: ignore
6
7
  start: bool = False
7
8
  ready: bool = False
8
9
  lock = Lock()
9
10
 
10
- def __init__(self):
11
- raise NotImplementedError()
12
-
13
11
 
14
12
  def install(*, value=logging.DEBUG // 2, force: bool = False) -> None:
15
13
  """
@@ -19,13 +19,13 @@ class SingletonType(type):
19
19
  Intercept all class definitions to prevent subclassing singleton types except for Singleton
20
20
  """
21
21
  if "__init_subclass__" in attrs:
22
- raise NotImplementedError("Singleton's should not be subclassed or implement __init_subclass__")
22
+ raise TypeError("Singleton's should not be subclassed or implement __init_subclass__")
23
23
 
24
24
  def _init_subclass(cls):
25
25
  with mcs._types_lock:
26
26
  if any(issubclass(cls, i) for i in mcs._types if i is not Singleton):
27
27
  err = "Do not derive from Singletons other than the base Singleton type"
28
- raise NotImplementedError(err)
28
+ raise TypeError(err)
29
29
  mcs._types.append(cls)
30
30
 
31
31
  attrs["__init_subclass__"] = _init_subclass
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zstdlib
3
- Version: 0.0.7
3
+ Version: 0.0.11
4
4
  Summary: A set of useful python utilities
5
5
  License: GPLv3
6
6
  Project-URL: Homepage, https://github.com/zwimer/zstdlib
7
7
  Keywords: remote,pipe
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
12
- Requires-Python: >=3.10
12
+ Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
 
@@ -6,6 +6,7 @@ tests/not_set.py
6
6
  tests/test_ansi.py
7
7
  tests/test_enum.py
8
8
  tests/test_frozen.py
9
+ tests/test_io.py
9
10
  tests/test_singleton.py
10
11
  tests/log/__init__.py
11
12
  tests/log/base.py
@@ -15,6 +16,7 @@ zstdlib/__init__.py
15
16
  zstdlib/ansi.py
16
17
  zstdlib/enum.py
17
18
  zstdlib/frozen.py
19
+ zstdlib/io.py
18
20
  zstdlib/not_set.py
19
21
  zstdlib/py.typed
20
22
  zstdlib/singleton.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes