zstdlib 0.6.0__tar.gz → 0.7.0__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 (32) hide show
  1. {zstdlib-0.6.0 → zstdlib-0.7.0}/PKG-INFO +1 -1
  2. {zstdlib-0.6.0 → zstdlib-0.7.0}/pyproject.toml +39 -12
  3. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/log/base.py +13 -11
  4. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/log/test_trace.py +1 -2
  5. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/test_ansi.py +9 -0
  6. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/test_enum.py +4 -3
  7. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/test_frozen.py +38 -40
  8. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/test_io.py +16 -17
  9. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/test_not_set.py +1 -1
  10. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/test_singleton.py +10 -10
  11. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/__init__.py +1 -1
  12. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/ansi.py +32 -23
  13. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/enum.py +31 -31
  14. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/frozen.py +18 -15
  15. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/io.py +46 -59
  16. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/log/cute.py +19 -21
  17. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/log/trace.py +14 -14
  18. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/not_set.py +1 -2
  19. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/signal_cm.py +8 -7
  20. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/singleton.py +21 -23
  21. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib.egg-info/PKG-INFO +1 -1
  22. {zstdlib-0.6.0 → zstdlib-0.7.0}/LICENSE +0 -0
  23. {zstdlib-0.6.0 → zstdlib-0.7.0}/README.md +0 -0
  24. {zstdlib-0.6.0 → zstdlib-0.7.0}/setup.cfg +0 -0
  25. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/__init__.py +0 -0
  26. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/log/__init__.py +0 -0
  27. {zstdlib-0.6.0 → zstdlib-0.7.0}/tests/log/test_cute.py +0 -0
  28. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/log/__init__.py +0 -0
  29. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib/py.typed +0 -0
  30. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib.egg-info/SOURCES.txt +0 -0
  31. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib.egg-info/dependency_links.txt +0 -0
  32. {zstdlib-0.6.0 → zstdlib-0.7.0}/zstdlib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zstdlib
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: A set of useful python utilities
5
5
  License: GPLv3
6
6
  Project-URL: Homepage, https://github.com/zwimer/zstdlib
@@ -5,10 +5,10 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "zstdlib"
7
7
  classifiers = [
8
- "Programming Language :: Python :: 3",
9
- "Programming Language :: Python :: 3.14",
10
- "Development Status :: 5 - Production/Stable",
11
- "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
8
+ "Programming Language :: Python :: 3",
9
+ "Programming Language :: Python :: 3.14",
10
+ "Development Status :: 5 - Production/Stable",
11
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
12
12
  ]
13
13
  license = {text = "GPLv3"}
14
14
  description = "A set of useful python utilities"
@@ -39,12 +39,12 @@ attr = "zstdlib.__version__"
39
39
  ignore-paths = '^tests/.*$'
40
40
  [tool.pylint."MESSAGES CONTROL"]
41
41
  disable = [
42
- "unnecessary-lambda-assignment",
43
- "method-cache-max-size-none",
44
- "missing-module-docstring",
45
- "too-few-public-methods",
46
- "line-too-long",
47
- "invalid-name"
42
+ "unnecessary-lambda-assignment",
43
+ "method-cache-max-size-none",
44
+ "missing-module-docstring",
45
+ "too-few-public-methods",
46
+ "line-too-long",
47
+ "invalid-name"
48
48
  ]
49
49
 
50
50
  [tool.black]
@@ -54,10 +54,37 @@ target-version = ["py314"]
54
54
  [tool.ruff]
55
55
  line-length = 110
56
56
  [tool.ruff.lint]
57
- ignore=["E731", "I001"]
57
+ ignore=[
58
+ "ANN401",
59
+ "D10", "D2", "D403", "D415",
60
+ "E731",
61
+ "I001",
62
+ "PLR0913", "PLR2004",
63
+ "S101",
64
+ "TID252",
65
+ ]
66
+ extend-select = [
67
+ "ANN",
68
+ "ARG",
69
+ "ASYNC",
70
+ "C4",
71
+ "D",
72
+ "DTZ",
73
+ "PERF",
74
+ "PGH",
75
+ "PIE",
76
+ "PL",
77
+ "PTH",
78
+ "RET",
79
+ "S",
80
+ "SIM",
81
+ "TID"
82
+ ]
83
+ [tool.ruff.lint.pydocstyle]
84
+ convention = "google"
58
85
  [tool.ruff.lint.per-file-ignores]
59
86
  "__init__.py" = ["F401", "F403"]
60
- "tests/*" = ["DTZ005"]
87
+ "tests/*" = ["DTZ005", "PGH003", "PLW0108"]
61
88
 
62
89
  [tool.bandit]
63
90
  skips = ["B101", "B104", "B201"]
@@ -1,6 +1,6 @@
1
+ from contextlib import AbstractContextManager, contextmanager
2
+ from collections.abc import Callable, Iterator
1
3
  from logging.handlers import QueueHandler
2
- from contextlib import contextmanager
3
- from collections.abc import Callable
4
4
  from typing import ClassVar, Any
5
5
  from threading import Lock
6
6
  import logging
@@ -8,7 +8,7 @@ import queue
8
8
 
9
9
 
10
10
  @contextmanager
11
- def _cml(logger: logging.Logger, exit_func: Callable[[], Any]):
11
+ def _cml(logger: logging.Logger, exit_func: Callable[[], Any]) -> Iterator[logging.Logger]:
12
12
  try:
13
13
  yield logger
14
14
  finally:
@@ -16,9 +16,7 @@ def _cml(logger: logging.Logger, exit_func: Callable[[], Any]):
16
16
 
17
17
 
18
18
  class LeftBase:
19
- """
20
- A base class for tests that hijack loggers
21
- """
19
+ """A base class for tests that hijack loggers"""
22
20
 
23
21
  _hijacked: ClassVar[set[logging.Logger]] = set() # Set of all loggers that have ever been hijacked
24
22
  _qmap: ClassVar[dict[logging.Logger, queue.Queue[logging.LogRecord]]] = {}
@@ -28,9 +26,12 @@ class LeftBase:
28
26
  messages: ClassVar[dict[logging.Logger, list[str]]] = {}
29
27
 
30
28
  @classmethod
31
- def hijack(cls, name: str, reuse: bool = False, fmt: logging.Formatter | None = None):
29
+ def hijack(
30
+ cls, name: str, reuse: bool = False, fmt: logging.Formatter | None = None
31
+ ) -> AbstractContextManager[logging.Logger]:
32
32
  """
33
- :return: A logger configured to write to an internal queue
33
+ Returns:
34
+ A logger configured to write to an internal queue
34
35
  """
35
36
  log = logging.getLogger(name)
36
37
  q: queue.Queue[logging.LogRecord] = queue.Queue()
@@ -48,9 +49,10 @@ class LeftBase:
48
49
 
49
50
  @classmethod
50
51
  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
52
+ """Restore the logger and read all messages from the queue
53
+
54
+ Returns:
55
+ The messages stored by the logger queue
54
56
  """
55
57
  if len(logger.handlers) != 1 or not isinstance(qh := logger.handlers[0], QueueHandler):
56
58
  raise RuntimeError("Logger not hijacked")
@@ -11,8 +11,7 @@ from .base import LeftBase
11
11
  class TestTrace(LeftBase, unittest.TestCase):
12
12
 
13
13
  def test_trace(self) -> None:
14
- """
15
- Avoid splitting up into multiple functions since trace.install() affects global state
14
+ """Avoid splitting up into multiple functions since trace.install() affects global state
16
15
  Keeping this as one function ensures that the tests are run in order
17
16
  """
18
17
  # Test bad installs
@@ -51,6 +51,15 @@ class TestAnsiColor(unittest.TestCase):
51
51
  self.assertEqual(f"\033[31;42m{msg}\033[0m", Color.red_bg_green(msg))
52
52
  self.assertEqual(f"\033[1;3;32;101m{msg}\033[0m", Color.bg_bright_red_bold_italic_green(msg))
53
53
 
54
+ def test_hash(self) -> None:
55
+ c1 = Color("black")
56
+ c2 = Color("black")
57
+ other = Color("blue")
58
+ self.assertEqual(c1, c2)
59
+ self.assertNotEqual(c1, other)
60
+ self.assertEqual(hash(c1), hash(c2))
61
+ self.assertNotEqual(hash(c1), hash(other))
62
+
54
63
 
55
64
  if __name__ == "__main__":
56
65
  unittest.main()
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,unused-variable
2
+ from typing import Self
2
3
  import unittest
3
4
 
4
5
  from zstdlib.enum import EnumType, Enum, entries, values, auto
@@ -63,7 +64,7 @@ class TestEnumType(unittest.TestCase):
63
64
  class ET1(metaclass=EnumType):
64
65
  arg1: int = 1
65
66
 
66
- def __init__(self):
67
+ def __init__(self) -> None:
67
68
  pass
68
69
 
69
70
  with self.assertRaises(AttributeError):
@@ -71,8 +72,8 @@ class TestEnumType(unittest.TestCase):
71
72
  class ET2(metaclass=EnumType):
72
73
  arg1: int = 1
73
74
 
74
- def __new__(cls):
75
- pass
75
+ def __new__(cls) -> Self:
76
+ return super().__new__(cls)
76
77
 
77
78
  class ET3(metaclass=EnumType):
78
79
  arg1: int = 0
@@ -1,5 +1,5 @@
1
- # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,unused-variable,attribute-defined-outside-init
2
- from sys import version_info
1
+ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
2
+ from annotationlib import get_annotations
3
3
  import unittest
4
4
 
5
5
  from zstdlib.frozen import Freezable, frozen
@@ -7,22 +7,24 @@ from zstdlib.frozen import Freezable, frozen
7
7
 
8
8
  class TestFreezable(unittest.TestCase):
9
9
 
10
- def test_unfrozen(self):
10
+ def test_unfrozen(self) -> None:
11
11
  class F1(Freezable):
12
- pass
12
+ def __init__(self, a: int) -> None:
13
+ super().__init__()
14
+ self.a = a
13
15
 
14
- f1 = F1()
15
- f1.a = 1
16
+ f1 = F1(1)
16
17
  self.assertEqual(f1.a, 1)
17
18
  del f1.a
18
19
  self.assertIs(getattr(f1, "a", None), None)
19
20
 
20
- def test_frozen(self):
21
+ def test_frozen(self) -> None:
21
22
  class F1(Freezable):
22
- pass
23
+ def __init__(self, a: int) -> None:
24
+ super().__init__()
25
+ self.a = a
23
26
 
24
- f1 = F1()
25
- f1.a = 1
27
+ f1 = F1(1)
26
28
  f1.freeze()
27
29
  self.assertEqual(f1.a, 1)
28
30
  with self.assertRaises(AttributeError):
@@ -31,12 +33,13 @@ class TestFreezable(unittest.TestCase):
31
33
  del f1.a
32
34
  f1.freeze() # Should not raise an error
33
35
 
34
- def test_thaw(self):
36
+ def test_thaw(self) -> None:
35
37
  class F1(Freezable):
36
- pass
38
+ def __init__(self, a: int) -> None:
39
+ super().__init__()
40
+ self.a = a
37
41
 
38
- f1 = F1()
39
- f1.a = 1
42
+ f1 = F1(1)
40
43
  f1.freeze()
41
44
  self.assertEqual(f1.a, 1)
42
45
  f1.thaw()
@@ -45,12 +48,13 @@ class TestFreezable(unittest.TestCase):
45
48
  del f1.a
46
49
  self.assertIs(getattr(f1, "a", None), None)
47
50
 
48
- def test_permanent_freeze(self):
51
+ def test_permanent_freeze(self) -> None:
49
52
  class F1(Freezable):
50
- pass
53
+ def __init__(self, a: int) -> None:
54
+ super().__init__()
55
+ self.a = a
51
56
 
52
- f1 = F1()
53
- f1.a = 1
57
+ f1 = F1(1)
54
58
  f1.freeze() # To ensure that .freeze(permanent=True) works when already frozen too
55
59
  f1.freeze(permanent=True)
56
60
  f1.freeze(permanent=True) # Should not raise an error
@@ -60,7 +64,7 @@ class TestFreezable(unittest.TestCase):
60
64
  with self.assertRaises(AttributeError):
61
65
  f1.a = 1
62
66
 
63
- def test_properties(self):
67
+ def test_properties(self) -> None:
64
68
 
65
69
  class F1(Freezable):
66
70
  pass
@@ -78,10 +82,10 @@ class TestFreezable(unittest.TestCase):
78
82
 
79
83
  class TestFrozen(unittest.TestCase):
80
84
 
81
- def test_frozen(self):
85
+ def test_frozen(self) -> None:
82
86
  @frozen
83
87
  class F1:
84
- def __init__(self):
88
+ def __init__(self) -> None:
85
89
  self.a = 1
86
90
  self.b = 1
87
91
  del self.b
@@ -94,34 +98,28 @@ class TestFrozen(unittest.TestCase):
94
98
  with self.assertRaises(AttributeError):
95
99
  del f1.a
96
100
 
97
- def test_metadata(self):
101
+ def test_metadata(self) -> None:
98
102
  @frozen
99
103
  class F1:
100
104
  def __init__(self, a: int = 1, *, b: bool = False) -> None:
101
- """
102
- init doc
103
- """
105
+ """init doc"""
104
106
 
105
107
  f1 = F1()
106
- self.assertEqual(f1.__init__.__doc__.strip(), "init doc")
107
- self.assertEqual(f1.__init__.__name__, "__init__")
108
- self.assertEqual(f1.__init__.__qualname__, "TestFrozen.test_metadata.<locals>.F1.__init__")
109
- self.assertTupleEqual(f1.__init__.__defaults__, (1,))
110
- self.assertDictEqual(f1.__init__.__kwdefaults__, {"b": False})
111
- if version_info < (3, 13, 0.1) or hasattr(f1.__init__, "__annotations__"):
112
- self.assertDictEqual(f1.__init__.__annotations__, {"a": int, "b": bool, "return": None})
113
- else: # annotations changed in 3.14, .__annotations__ *might* not exist
114
- from annotationlib import get_annotations
115
-
116
- self.assertDictEqual(get_annotations(f1.__init__), {"a": int, "b": bool, "return": None})
117
-
118
- def test_frozen_custom(self):
108
+ f1_init = f1.__init__ # type: ignore[misc]
109
+ self.assertEqual(f1_init.__doc__.strip(), "init doc")
110
+ self.assertEqual(f1_init.__name__, "__init__")
111
+ self.assertEqual(f1_init.__qualname__, "TestFrozen.test_metadata.<locals>.F1.__init__")
112
+ self.assertTupleEqual(f1_init.__defaults__, (1,))
113
+ self.assertDictEqual(f1_init.__kwdefaults__, {"b": False})
114
+ self.assertDictEqual(get_annotations(f1_init), {"a": int, "b": bool, "return": None})
115
+
116
+ def test_frozen_custom(self) -> None:
119
117
  @frozen("custom")
120
118
  class F1:
121
- def __init__(self):
119
+ def __init__(self) -> None:
122
120
  self.a = 1
123
121
 
124
- def custom(self):
122
+ def custom(self) -> None:
125
123
  self.a = 2
126
124
 
127
125
  f1 = F1()
@@ -1,3 +1,4 @@
1
+ from typing import Any
1
2
  import tempfile
2
3
  import unittest
3
4
 
@@ -5,16 +6,14 @@ from zstdlib.io import ProtectedFile, BinaryIO, TextIO, io
5
6
 
6
7
 
7
8
  class RamFile(tempfile.SpooledTemporaryFile):
8
- """
9
- A file-like object that stores its contents in memory.
10
- """
9
+ """A file-like object that stores its contents in memory."""
11
10
 
12
- rollover = None # type: ignore
11
+ rollover = NotImplemented # Disallow rollover
13
12
 
14
- def __init__(self, *args, mode: str = "w+", **kwargs):
13
+ def __init__(self, *args: Any, mode: str = "w+", **kwargs: Any) -> None:
15
14
  super().__init__(10**9, *args, mode=mode, **kwargs)
16
15
 
17
- def load(self, x):
16
+ def load(self, x: str) -> None:
18
17
  self.write(x)
19
18
  self.flush()
20
19
  self.seek(self.tell() - len(x))
@@ -22,7 +21,7 @@ class RamFile(tempfile.SpooledTemporaryFile):
22
21
 
23
22
  class TestIO(unittest.TestCase):
24
23
 
25
- def test_init(self):
24
+ def test_init(self) -> None:
26
25
  with RamFile(mode="wb+") as fb, RamFile() as fs:
27
26
  ts = TextIO(fs)
28
27
  tb = BinaryIO(fb)
@@ -33,7 +32,7 @@ class TestIO(unittest.TestCase):
33
32
  self.assertIs(ts, io(fs))
34
33
  self.assertIs(tb, io(fb))
35
34
 
36
- def test_protected_file(self):
35
+ def test_protected_file(self) -> None:
37
36
  with RamFile() as f:
38
37
  p = ProtectedFile(f)
39
38
  self.assertIs(f, p.raw)
@@ -42,13 +41,13 @@ class TestIO(unittest.TestCase):
42
41
  with self.assertRaises(AttributeError):
43
42
  getattr(p, i)
44
43
 
45
- def test_unread(self):
44
+ def test_unread(self) -> None:
46
45
  with RamFile() as raw:
47
46
  f = io(raw)
48
- f.unread("hello")
47
+ f.unread("hello") # type: ignore[arg-type] # STF types as bytes regardless of mode :/
49
48
  self.assertEqual("hello", f._buffer)
50
49
 
51
- def test_read(self):
50
+ def test_read(self) -> None:
52
51
  with RamFile() as raw:
53
52
  raw.load("foobaz")
54
53
  f = io(raw)
@@ -56,18 +55,18 @@ class TestIO(unittest.TestCase):
56
55
  self.assertEqual("fo", f.read(2))
57
56
  self.assertEqual("o", f.read(1))
58
57
  # Test read of buffer
59
- f.unread("bar")
58
+ f.unread("bar") # type: ignore[arg-type] # STF types as bytes regardless of mode :/
60
59
  self.assertEqual("b", f.read(1))
61
60
  # Test read of file + buffer
62
- f.unread("foob")
61
+ f.unread("foob") # type: ignore[arg-type] # STF types as bytes regardless of mode :/
63
62
  self.assertEqual("foobar", f.read(6))
64
- f.unread("foobar")
63
+ f.unread("foobar") # type: ignore[arg-type] # STF types as bytes regardless of mode :/
65
64
  self.assertEqual("foobarbaz", f.read())
66
65
 
67
- def test_peek(self):
66
+ def test_peek(self) -> None:
68
67
  with RamFile() as raw:
69
68
  f = io(raw)
70
- f.unread("foo")
69
+ f.unread("foo") # type: ignore[arg-type] # STF types as bytes regardless of mode :/
71
70
  # Test peek of buffer
72
71
  for i in range(3):
73
72
  self.assertEqual("fo", f.peek(2))
@@ -78,7 +77,7 @@ class TestIO(unittest.TestCase):
78
77
  self.assertEqual("ba", f.peek(2))
79
78
  self.assertEqual("bar", f.read())
80
79
  # Test peek of both
81
- f.unread("baz")
80
+ f.unread("baz") # type: ignore[arg-type] # STF types as bytes regardless of mode :/
82
81
  raw.load("qux")
83
82
  for i in range(3):
84
83
  for k in range(5):
@@ -4,7 +4,7 @@ from zstdlib import NotSetType, NotSet
4
4
 
5
5
 
6
6
  class TestNotSet(unittest.TestCase):
7
- def test_not_set(self):
7
+ def test_not_set(self) -> None:
8
8
  self.assertIsInstance(NotSetType, type)
9
9
  self.assertIsInstance(NotSet, NotSetType)
10
10
  self.assertIsNot(None, NotSet)
@@ -2,6 +2,7 @@
2
2
  from collections.abc import Callable
3
3
  from threading import Thread, Lock
4
4
  from datetime import datetime
5
+ from typing import Any
5
6
  from time import sleep
6
7
  import unittest
7
8
 
@@ -32,12 +33,12 @@ class TestSingleton(unittest.TestCase):
32
33
  with self.assertRaises(TypeError): # Not ok to define __init_subclass__ in Singleton
33
34
 
34
35
  class ST3(Singleton):
35
- def __init_subclass__(cls, **kwargs):
36
+ def __init_subclass__(cls, **_: Any) -> None:
36
37
  pass
37
38
 
38
- def test_instance(self):
39
+ def test_instance(self) -> None:
39
40
  class ST(Singleton):
40
- def __init__(self, _):
41
+ def __init__(self, _: Any) -> None:
41
42
  pass
42
43
 
43
44
  self.assertTrue(issubclass(NoInstanceError, TypeError))
@@ -47,11 +48,10 @@ class TestSingleton(unittest.TestCase):
47
48
  self.assertIs(ST.instance(), obj)
48
49
 
49
50
  with self.assertRaises(TypeError):
50
- ST.instance(4)
51
+ ST.instance(4) # type: ignore[call-arg]
51
52
 
52
- def test_multi_thread(self):
53
- """
54
- Ensure that Singleton is thread safe and that constructing an object doesn't delay other threads
53
+ def test_multi_thread(self) -> None:
54
+ """Ensure that Singleton is thread safe and that constructing an object doesn't delay other threads
55
55
  Technically this is more of a heuristic, but failing is extremely unlikely if this is thread safe
56
56
  Verifies that classes can construct concurrently, thread-safely, and only one instance is created
57
57
  """
@@ -96,11 +96,11 @@ class TestSingleton(unittest.TestCase):
96
96
 
97
97
  # Verify concurrent construction of ST1 and ST2
98
98
  class Fast(Singleton):
99
- def __init__(self):
99
+ def __init__(self) -> None:
100
100
  sleep(0.3)
101
101
 
102
102
  class Slow(Singleton):
103
- def __init__(self):
103
+ def __init__(self) -> None:
104
104
  sleep(0.8)
105
105
 
106
106
  run_all(simple("f1", Slow, 0), simple("f2", Fast, 0.2))
@@ -120,7 +120,7 @@ class TestSingleton(unittest.TestCase):
120
120
  class ST(Singleton):
121
121
  first = True
122
122
 
123
- def __init__(self):
123
+ def __init__(self) -> None:
124
124
  if self.first:
125
125
  self.first = False
126
126
  sleep(0.4)
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.0"
1
+ __version__ = "0.7.0"
2
2
 
3
3
  from .not_set import NotSetType, NotSet
4
4
  from .frozen import Freezable, frozen
@@ -10,9 +10,7 @@ _PREFIX = "\033["
10
10
 
11
11
  @unique
12
12
  class PureColor(Enum):
13
- """
14
- An enum of ansi color values
15
- """
13
+ """An enum of ansi color values"""
16
14
 
17
15
  black = 30
18
16
  red = auto()
@@ -26,13 +24,13 @@ class PureColor(Enum):
26
24
 
27
25
 
28
26
  class RawColor:
29
- """
30
- A class that represents a color with all possible modifiers
31
- """
27
+ """A class that represents a color with all possible modifiers"""
32
28
 
33
29
  __slots__ = ("background", "bright", "color", "value")
34
30
 
35
- def __init__(self, color: PureColor | str | int, *, bright: bool = False, background: bool = False):
31
+ def __init__(
32
+ self, color: PureColor | str | int, *, bright: bool = False, background: bool = False
33
+ ) -> None:
36
34
  self.color = PureColor(getattr(PureColor, color) if isinstance(color, str) else color)
37
35
  self.bright = bright
38
36
  self.background = background
@@ -68,9 +66,7 @@ class RawColor:
68
66
 
69
67
 
70
68
  class _ColorMeta(type):
71
- """
72
- A metaclass that implements __getattr__ at the class level for Color
73
- """
69
+ """A metaclass that implements __getattr__ at the class level for Color"""
74
70
 
75
71
  def __getattr__(cls, item: str) -> Color:
76
72
  try:
@@ -80,8 +76,7 @@ class _ColorMeta(type):
80
76
 
81
77
 
82
78
  class Color(metaclass=_ColorMeta):
83
- """
84
- Represents an Ansi color code; calling this on a string will apply the code to the string
79
+ """Represents an Ansi color code; calling this on a string will apply the code to the string
85
80
  Can be constructed via .factory; alternatively calling Color.<name> or Color(<name>) will
86
81
  automatically construct a color. Colors are constructed with attributes determined by
87
82
  splitting <name> on "_". Colors may be made bright by prepending "bright_" to the color name
@@ -104,10 +99,11 @@ class Color(metaclass=_ColorMeta):
104
99
  RESET: str = f"{_PREFIX}0m"
105
100
 
106
101
  def __init__(self, fmt: Self | RawColor | str = "", *, code: str = "") -> None:
107
- """
108
- Create a Color based on either the format or code, exactly one must be passed
109
- :param fmt: A Color, RawColor, or string representation of this desired color
110
- :param code: The ansi color code this object represents
102
+ """Create a Color based on either the format or code, exactly one must be passed
103
+
104
+ Args:
105
+ fmt: A Color, RawColor, or string representation of this desired color
106
+ code: The ansi color code this object represents
111
107
  """
112
108
  if fmt != "" and code != "":
113
109
  raise ValueError("code and fmt may not both be passed")
@@ -122,13 +118,15 @@ class Color(metaclass=_ColorMeta):
122
118
 
123
119
  def __call__(self, string: str) -> str:
124
120
  """
125
- :return: The string color codes with the given ansi code
121
+ Returns:
122
+ The string color codes with the given ansi code
126
123
  """
127
124
  return f"{self.code}{string}{self.RESET}"
128
125
 
129
126
  def __add__(self, other: Self) -> Self:
130
127
  """
131
- :return: A new color made by concatenating these two
128
+ Returns:
129
+ A new color made by concatenating these two
132
130
  """
133
131
  if not isinstance(other, type(self)):
134
132
  raise TypeError("Cannot add non-Color to Color")
@@ -136,22 +134,32 @@ class Color(metaclass=_ColorMeta):
136
134
 
137
135
  def __eq__(self, other: object) -> bool:
138
136
  """
139
- :return: Whether the two colors are equal
137
+ Returns:
138
+ Whether the two colors are equal
140
139
  """
141
140
  return isinstance(other, type(self)) and self.code == other.code
142
141
 
142
+ def __hash__(self) -> int:
143
+ """
144
+ Returns:
145
+ A hash of the current color
146
+ """
147
+ return hash((type(self), self.code))
148
+
143
149
  def __repr__(self) -> str:
144
150
  """
145
- :return: The ansi color code this object represents
151
+ Returns:
152
+ The ansi color code this object represents
146
153
  """
147
154
  return f"<Color {self.code!r}>"
148
155
 
149
156
  @classmethod
150
157
  def factory(
151
- cls, *, foreground: RawColor | None = None, background: RawColor | None = None, **modifiers
158
+ cls, *, foreground: RawColor | None = None, background: RawColor | None = None, **modifiers: bool
152
159
  ) -> Self:
153
160
  """
154
- :return: The Ansi given the chosen color and various modifiers
161
+ Returns:
162
+ The Ansi given the chosen color and various modifiers
155
163
  """
156
164
  return cls(code=_generate_code(foreground, background, modifiers))
157
165
 
@@ -160,7 +168,8 @@ def _generate_code(
160
168
  foreground: RawColor | None, background: RawColor | None, modifiers: dict[str, bool]
161
169
  ) -> str:
162
170
  """
163
- :return: The Ansi given the chosen color and various modifiers
171
+ Returns:
172
+ The Ansi given the chosen color and various modifiers
164
173
  """
165
174
  if any(bad := [i for i in modifiers if i not in MODIFIERS]):
166
175
  raise ValueError(f"Unknown modifier(s): {bad}")