zstdlib 0.0.11__tar.gz → 0.2.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.
- {zstdlib-0.0.11 → zstdlib-0.2.0}/PKG-INFO +3 -2
- zstdlib-0.2.0/tests/test_enum.py +133 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/test_frozen.py +32 -14
- zstdlib-0.2.0/tests/test_singleton.py +139 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/__init__.py +2 -2
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/ansi.py +1 -1
- zstdlib-0.2.0/zstdlib/enum.py +154 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/frozen.py +33 -19
- zstdlib-0.2.0/zstdlib/singleton.py +80 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib.egg-info/PKG-INFO +3 -2
- zstdlib-0.0.11/tests/test_enum.py +0 -137
- zstdlib-0.0.11/tests/test_singleton.py +0 -126
- zstdlib-0.0.11/zstdlib/enum.py +0 -53
- zstdlib-0.0.11/zstdlib/singleton.py +0 -58
- {zstdlib-0.0.11 → zstdlib-0.2.0}/LICENSE +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/README.md +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/pyproject.toml +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/setup.cfg +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/__init__.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/log/__init__.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/log/base.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/log/test_cute.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/log/test_trace.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/not_set.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/test_ansi.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/test_io.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/io.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/log/__init__.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/log/cute.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/log/trace.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/not_set.py +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/py.typed +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib.egg-info/SOURCES.txt +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib.egg-info/dependency_links.txt +0 -0
- {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: zstdlib
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A set of useful python utilities
|
|
5
5
|
License: GPLv3
|
|
6
6
|
Project-URL: Homepage, https://github.com/zwimer/zstdlib
|
|
@@ -12,6 +12,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
15
16
|
|
|
16
17
|
# zstdlib
|
|
17
18
|
A set of useful python utilities
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,unused-variable
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from zstdlib.enum import EnumType, Enum, entries, values, auto
|
|
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 ET1(metaclass=EnumType, empty_ok=True):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
with self.assertRaises(ValueError):
|
|
19
|
+
|
|
20
|
+
class ET2(metaclass=EnumType):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def test_dupes(self) -> None:
|
|
24
|
+
class ET1(metaclass=EnumType, dupes_ok=True):
|
|
25
|
+
a: int = 5
|
|
26
|
+
b: int = 5
|
|
27
|
+
c: dict = {}
|
|
28
|
+
|
|
29
|
+
with self.assertRaises(ValueError):
|
|
30
|
+
|
|
31
|
+
class ET2(metaclass=EnumType):
|
|
32
|
+
a: int = 5
|
|
33
|
+
b: int = 5
|
|
34
|
+
|
|
35
|
+
with self.assertRaises(TypeError):
|
|
36
|
+
|
|
37
|
+
class ET3(metaclass=EnumType):
|
|
38
|
+
a: dict = {}
|
|
39
|
+
|
|
40
|
+
def test_type_check(self) -> None:
|
|
41
|
+
class ET1(metaclass=EnumType, type_check=set()):
|
|
42
|
+
a: str = 5 # type: ignore[assignment]
|
|
43
|
+
|
|
44
|
+
with self.assertRaises(TypeError):
|
|
45
|
+
|
|
46
|
+
class ET2(metaclass=EnumType):
|
|
47
|
+
a: str = 5 # type: ignore[assignment]
|
|
48
|
+
|
|
49
|
+
def test_annotations(self) -> None:
|
|
50
|
+
with self.assertRaises(ValueError):
|
|
51
|
+
|
|
52
|
+
class ET1(metaclass=EnumType):
|
|
53
|
+
arg1 = 1
|
|
54
|
+
|
|
55
|
+
with self.assertRaises(ValueError):
|
|
56
|
+
|
|
57
|
+
class ET2(metaclass=EnumType):
|
|
58
|
+
arg1: int
|
|
59
|
+
|
|
60
|
+
def test_instantiation(self) -> None:
|
|
61
|
+
with self.assertRaises(AttributeError):
|
|
62
|
+
|
|
63
|
+
class ET1(metaclass=EnumType):
|
|
64
|
+
arg1: int = 1
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
with self.assertRaises(AttributeError):
|
|
70
|
+
|
|
71
|
+
class ET2(metaclass=EnumType):
|
|
72
|
+
arg1: int = 1
|
|
73
|
+
|
|
74
|
+
def __new__(cls):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
class ET3(metaclass=EnumType):
|
|
78
|
+
arg1: int = 0
|
|
79
|
+
|
|
80
|
+
with self.assertRaises(TypeError):
|
|
81
|
+
ET3()
|
|
82
|
+
|
|
83
|
+
with self.assertRaises(TypeError):
|
|
84
|
+
ET3.__new__({}) # type: ignore[arg-type]
|
|
85
|
+
|
|
86
|
+
def test_iter(self) -> None:
|
|
87
|
+
|
|
88
|
+
class ET11(metaclass=EnumType):
|
|
89
|
+
arg1: int = 0
|
|
90
|
+
arg2: str = "1"
|
|
91
|
+
|
|
92
|
+
self.assertEqual(tuple(ET11), ("arg1", "arg2"))
|
|
93
|
+
|
|
94
|
+
def test_auto(self) -> None:
|
|
95
|
+
|
|
96
|
+
class ET1(metaclass=EnumType):
|
|
97
|
+
arg1: int = 0
|
|
98
|
+
arg2 = auto
|
|
99
|
+
arg3 = auto
|
|
100
|
+
|
|
101
|
+
self.assertEqual(3, len(set(values(ET1))))
|
|
102
|
+
|
|
103
|
+
with self.assertRaises(TypeError):
|
|
104
|
+
|
|
105
|
+
class ET2(metaclass=EnumType):
|
|
106
|
+
arg1: int = 0
|
|
107
|
+
arg2: str = auto # type: ignore[assignment]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestEnum(unittest.TestCase):
|
|
111
|
+
|
|
112
|
+
def test_is(self) -> None:
|
|
113
|
+
self.assertIsInstance(Enum, EnumType)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestFunctions(unittest.TestCase):
|
|
117
|
+
def test_entries(self) -> None:
|
|
118
|
+
class E1(Enum):
|
|
119
|
+
arg1: int = 0
|
|
120
|
+
arg2: str = "1"
|
|
121
|
+
|
|
122
|
+
self.assertEqual(entries(E1), {"arg1": (int, 0), "arg2": (str, "1")})
|
|
123
|
+
|
|
124
|
+
def test_values(self) -> None:
|
|
125
|
+
class E1(Enum):
|
|
126
|
+
arg1: int = 0
|
|
127
|
+
arg2: str = "1"
|
|
128
|
+
|
|
129
|
+
self.assertEqual(values(E1), (0, "1"))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
unittest.main()
|
|
@@ -28,6 +28,7 @@ class TestFreezable(unittest.TestCase):
|
|
|
28
28
|
f1.a = 2
|
|
29
29
|
with self.assertRaises(AttributeError):
|
|
30
30
|
del f1.a
|
|
31
|
+
f1.freeze() # Should not raise an error
|
|
31
32
|
|
|
32
33
|
def test_thaw(self):
|
|
33
34
|
class F1(Freezable):
|
|
@@ -49,13 +50,30 @@ class TestFreezable(unittest.TestCase):
|
|
|
49
50
|
|
|
50
51
|
f1 = F1()
|
|
51
52
|
f1.a = 1
|
|
53
|
+
f1.freeze() # To ensure that .freeze(permanent=True) works when already frozen too
|
|
52
54
|
f1.freeze(permanent=True)
|
|
55
|
+
f1.freeze(permanent=True) # Should not raise an error
|
|
53
56
|
self.assertEqual(f1.a, 1)
|
|
54
57
|
with self.assertRaises(RuntimeError):
|
|
55
58
|
f1.thaw()
|
|
56
59
|
with self.assertRaises(AttributeError):
|
|
57
60
|
f1.a = 1
|
|
58
61
|
|
|
62
|
+
def test_properties(self):
|
|
63
|
+
|
|
64
|
+
class F1(Freezable):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
f1 = F1()
|
|
68
|
+
self.assertFalse(f1.frozen)
|
|
69
|
+
self.assertTrue(f1.thawable)
|
|
70
|
+
f1.freeze()
|
|
71
|
+
self.assertTrue(f1.frozen)
|
|
72
|
+
self.assertTrue(f1.thawable)
|
|
73
|
+
f1.freeze(permanent=True)
|
|
74
|
+
self.assertTrue(f1.frozen)
|
|
75
|
+
self.assertFalse(f1.thawable)
|
|
76
|
+
|
|
59
77
|
|
|
60
78
|
class TestFrozen(unittest.TestCase):
|
|
61
79
|
|
|
@@ -77,35 +95,35 @@ class TestFrozen(unittest.TestCase):
|
|
|
77
95
|
|
|
78
96
|
def test_metadata(self):
|
|
79
97
|
@frozen
|
|
80
|
-
class
|
|
98
|
+
class F1:
|
|
81
99
|
def __init__(self, a: int = 1, *, b: bool = False) -> None:
|
|
82
100
|
"""
|
|
83
101
|
init doc
|
|
84
102
|
"""
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
self.assertEqual(
|
|
88
|
-
self.assertEqual(
|
|
89
|
-
self.assertEqual(
|
|
90
|
-
self.assertTupleEqual(
|
|
91
|
-
self.assertDictEqual(
|
|
92
|
-
self.assertDictEqual(
|
|
104
|
+
f1 = F1()
|
|
105
|
+
self.assertEqual(f1.__init__.__doc__.strip(), "init doc")
|
|
106
|
+
self.assertEqual(f1.__init__.__name__, "__init__")
|
|
107
|
+
self.assertEqual(f1.__init__.__qualname__, "TestFrozen.test_metadata.<locals>.F2.__init__")
|
|
108
|
+
self.assertTupleEqual(f1.__init__.__defaults__, (1,))
|
|
109
|
+
self.assertDictEqual(f1.__init__.__kwdefaults__, {"b": False})
|
|
110
|
+
self.assertDictEqual(f1.__init__.__annotations__, {"a": int, "b": bool, "return": None})
|
|
93
111
|
|
|
94
112
|
def test_frozen_custom(self):
|
|
95
113
|
@frozen("custom")
|
|
96
|
-
class
|
|
114
|
+
class F1:
|
|
97
115
|
def __init__(self):
|
|
98
116
|
self.a = 1
|
|
99
117
|
|
|
100
118
|
def custom(self):
|
|
101
119
|
self.a = 2
|
|
102
120
|
|
|
103
|
-
|
|
104
|
-
self.assertEqual(
|
|
105
|
-
|
|
106
|
-
self.assertEqual(
|
|
121
|
+
f1 = F1()
|
|
122
|
+
self.assertEqual(f1.a, 1)
|
|
123
|
+
f1.custom()
|
|
124
|
+
self.assertEqual(f1.a, 2)
|
|
107
125
|
with self.assertRaises(AttributeError):
|
|
108
|
-
|
|
126
|
+
f1.a = 1
|
|
109
127
|
|
|
110
128
|
|
|
111
129
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,unused-variable
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from threading import Thread, Lock
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from time import sleep
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from zstdlib.singleton import NoInstanceError, Singleton
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestSingleton(unittest.TestCase):
|
|
12
|
+
|
|
13
|
+
def test_valid(self) -> None:
|
|
14
|
+
class ST1(Singleton):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class ST2(Singleton):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
self.assertIs(ST1(), ST1())
|
|
21
|
+
self.assertIsNot(ST1(), ST2())
|
|
22
|
+
|
|
23
|
+
def test_subclass(self) -> None:
|
|
24
|
+
class ST1(Singleton): # Ok to subtype Singleton base class
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
with self.assertRaises(TypeError): # Not ok to subtype a subtype of Singleton
|
|
28
|
+
|
|
29
|
+
class ST2(ST1):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
with self.assertRaises(TypeError): # Not ok to define __init_subclass__ in Singleton
|
|
33
|
+
|
|
34
|
+
class ST3(Singleton):
|
|
35
|
+
def __init_subclass__(cls, **kwargs):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def test_instance(self):
|
|
39
|
+
class ST(Singleton):
|
|
40
|
+
def __init__(self, _):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
self.assertTrue(issubclass(NoInstanceError, TypeError))
|
|
44
|
+
with self.assertRaises(NoInstanceError):
|
|
45
|
+
_ = ST.instance()
|
|
46
|
+
obj = ST(0)
|
|
47
|
+
self.assertIs(ST.instance(), obj)
|
|
48
|
+
|
|
49
|
+
with self.assertRaises(TypeError):
|
|
50
|
+
ST.instance(4)
|
|
51
|
+
|
|
52
|
+
def test_multi_thread(self):
|
|
53
|
+
"""
|
|
54
|
+
Ensure that Singleton is thread safe and that constructing an object doesn't delay other threads
|
|
55
|
+
Technically this is more of a heuristic, but failing is extremely unlikely if this is thread safe
|
|
56
|
+
Verifies that classes can construct concurrently, thread-safely, and only one instance is created
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Shared state for threads
|
|
60
|
+
start = [datetime.now()]
|
|
61
|
+
lock = Lock()
|
|
62
|
+
events = []
|
|
63
|
+
results = []
|
|
64
|
+
|
|
65
|
+
# Helper functions that can help define thread tests
|
|
66
|
+
timestamp = lambda: (datetime.now() - start[0]).total_seconds()
|
|
67
|
+
|
|
68
|
+
def mk_cls(cls: type, name: str) -> None:
|
|
69
|
+
fmt = lambda x: f"{timestamp():.1f}: {name} {x} {cls.__name__}()"
|
|
70
|
+
events.append(fmt("->"))
|
|
71
|
+
results.append(cls())
|
|
72
|
+
events.append(fmt("<-"))
|
|
73
|
+
|
|
74
|
+
def simple(name: str, cls: type, delay: float) -> Callable:
|
|
75
|
+
def _t() -> None:
|
|
76
|
+
with lock: # Ensure all threads can start simultaneously
|
|
77
|
+
pass
|
|
78
|
+
sleep(delay)
|
|
79
|
+
mk_cls(cls, name)
|
|
80
|
+
|
|
81
|
+
return _t
|
|
82
|
+
|
|
83
|
+
# Run all functions
|
|
84
|
+
def run_all(*funcs: Callable) -> None:
|
|
85
|
+
ts = [Thread(target=i) for i in funcs]
|
|
86
|
+
events.clear()
|
|
87
|
+
results.clear()
|
|
88
|
+
with lock:
|
|
89
|
+
for i in ts:
|
|
90
|
+
i.start()
|
|
91
|
+
# Give threads a moment to construct then let them go
|
|
92
|
+
sleep(0.2)
|
|
93
|
+
start[0] = datetime.now()
|
|
94
|
+
for i in ts:
|
|
95
|
+
i.join()
|
|
96
|
+
|
|
97
|
+
# Verify concurrent construction of ST1 and ST2
|
|
98
|
+
class Fast(Singleton):
|
|
99
|
+
def __init__(self):
|
|
100
|
+
sleep(0.3)
|
|
101
|
+
|
|
102
|
+
class Slow(Singleton):
|
|
103
|
+
def __init__(self):
|
|
104
|
+
sleep(0.8)
|
|
105
|
+
|
|
106
|
+
run_all(simple("f1", Slow, 0), simple("f2", Fast, 0.2))
|
|
107
|
+
want = [
|
|
108
|
+
"0.0: f1 -> Slow()",
|
|
109
|
+
"0.2: f2 -> Fast()",
|
|
110
|
+
"0.5: f2 <- Fast()",
|
|
111
|
+
"0.8: f1 <- Slow()",
|
|
112
|
+
]
|
|
113
|
+
self.assertEqual(want, events)
|
|
114
|
+
self.assertEqual(len(results), 2)
|
|
115
|
+
self.assertIs(results[0], Fast())
|
|
116
|
+
self.assertIs(results[1], Slow())
|
|
117
|
+
del Slow, Fast # Cleanup, for safety
|
|
118
|
+
|
|
119
|
+
# Verify that multiple threads constructing the same class only construct one object
|
|
120
|
+
class ST(Singleton):
|
|
121
|
+
first = True
|
|
122
|
+
|
|
123
|
+
def __init__(self):
|
|
124
|
+
if self.first:
|
|
125
|
+
self.first = False
|
|
126
|
+
sleep(0.4)
|
|
127
|
+
|
|
128
|
+
run_all(simple("f1", ST, 0), simple("f2", ST, 0.2))
|
|
129
|
+
self.assertEqual(len(events), 4)
|
|
130
|
+
self.assertEqual(events[:2], ["0.0: f1 -> ST()", "0.2: f2 -> ST()"])
|
|
131
|
+
# When the first thread's init finishes, the second will stop blocking and finish
|
|
132
|
+
self.assertEqual(len(results), 2)
|
|
133
|
+
for i in results:
|
|
134
|
+
self.assertIs(i, ST())
|
|
135
|
+
del ST # Cleanup, for safety
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
unittest.main()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class _Auto:
|
|
6
|
+
"""A sentinel value for automatic enum values"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Pass this as a value to have it automatically assigned
|
|
10
|
+
auto = _Auto()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnumType(type):
|
|
14
|
+
"""
|
|
15
|
+
Metaclass for uninstantiable Enum classes with required annotations and unique values
|
|
16
|
+
Enum entry names may not be prefixed with __ or be an EnumType._AutoValue
|
|
17
|
+
These 'Enum' classes may not be modified after creation
|
|
18
|
+
Items of annotation types in type_check will be type-checked
|
|
19
|
+
Duplicate values (as determined by set()) are not allowed unless dupes_ok is True
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_AUTO_PREFIX = "Enum_Auto_"
|
|
23
|
+
_TC_DEFAULT: set[type | None] = {int, str, float, bool, complex, bytes, None, type(None)}
|
|
24
|
+
|
|
25
|
+
class _AutoValue:
|
|
26
|
+
"""A class representing an automatically assigned enum value"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, value: int) -> None:
|
|
29
|
+
self._value = value
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return f"<Auto: {self._value}>"
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return str(self)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _gen_entries_and_annotations(
|
|
39
|
+
mcs, attrs: dict, dupes_ok: bool
|
|
40
|
+
) -> tuple[dict[str, tuple[type | None, Any]], dict[str, type | None]]:
|
|
41
|
+
"""
|
|
42
|
+
Generate enum entries from attrs, checking for duplicates and missing annotations
|
|
43
|
+
Does not check for empty enum or type correctness
|
|
44
|
+
"""
|
|
45
|
+
seen = set()
|
|
46
|
+
auto_val = 0
|
|
47
|
+
_entries = {}
|
|
48
|
+
_annotations = dict(attrs.get("__annotations__", {}))
|
|
49
|
+
try:
|
|
50
|
+
for a_name, value in ((i, k) for i, k in attrs.items() if not i.startswith("__")):
|
|
51
|
+
if value is auto:
|
|
52
|
+
auto_val += 1
|
|
53
|
+
value = mcs._AutoValue(auto_val)
|
|
54
|
+
if a_name in _annotations:
|
|
55
|
+
raise TypeError("Auto enum entries may not be type annotated")
|
|
56
|
+
_annotations[a_name] = mcs._AutoValue
|
|
57
|
+
elif isinstance(value, mcs._AutoValue):
|
|
58
|
+
raise ValueError("Enum entries may not be of type EnumType._AutoValue")
|
|
59
|
+
if not dupes_ok:
|
|
60
|
+
if value in seen:
|
|
61
|
+
raise ValueError(f"Duplicate enum value: {value}")
|
|
62
|
+
seen.add(value)
|
|
63
|
+
_entries[a_name] = (_annotations[a_name], value)
|
|
64
|
+
except TypeError:
|
|
65
|
+
raise TypeError(f"Unhashable enum value: {value}") from None
|
|
66
|
+
except KeyError:
|
|
67
|
+
raise ValueError("Non-auto enum entries must be type annotated") from None
|
|
68
|
+
return _entries, _annotations
|
|
69
|
+
|
|
70
|
+
# pylint: disable=too-many-arguments, dangerous-default-value
|
|
71
|
+
def __new__(
|
|
72
|
+
mcs,
|
|
73
|
+
name,
|
|
74
|
+
bases,
|
|
75
|
+
attrs,
|
|
76
|
+
*,
|
|
77
|
+
empty_ok: bool = False,
|
|
78
|
+
dupes_ok: bool = False,
|
|
79
|
+
type_check: set[type | None] = _TC_DEFAULT,
|
|
80
|
+
**kwargs,
|
|
81
|
+
):
|
|
82
|
+
# Disallow undesired instance methods
|
|
83
|
+
if bad := attrs.keys() & {"__init__", "__new__", "__entries__"}:
|
|
84
|
+
raise AttributeError(f"Illegal methods in Enum class: {', '.join(bad)}")
|
|
85
|
+
# Generate entries
|
|
86
|
+
_entries, _annotations = mcs._gen_entries_and_annotations(attrs, dupes_ok)
|
|
87
|
+
# Validate entries
|
|
88
|
+
if no_value := (_annotations.keys() - _entries.keys()):
|
|
89
|
+
raise ValueError(f"Entries must have a value: {' '.join(no_value)}")
|
|
90
|
+
if (not empty_ok) and not _entries:
|
|
91
|
+
raise ValueError("Enum type may not be empty")
|
|
92
|
+
if type_check:
|
|
93
|
+
for a_name, (ann, val) in _entries.items():
|
|
94
|
+
if ann in type_check and (
|
|
95
|
+
(ann is None is not val) or (ann is not None and not isinstance(val, ann))
|
|
96
|
+
):
|
|
97
|
+
raise TypeError(f"Entry {a_name} is not of type {ann}")
|
|
98
|
+
# Construct class
|
|
99
|
+
attrs["__entries__"] = _entries
|
|
100
|
+
attrs["__annotations__"] = _annotations
|
|
101
|
+
attrs["__init__"] = attrs["__new__"] = NotImplemented
|
|
102
|
+
return type.__new__(mcs, name, bases, attrs, **kwargs)
|
|
103
|
+
|
|
104
|
+
# Disallow modification
|
|
105
|
+
|
|
106
|
+
def __delattr__(cls, *_):
|
|
107
|
+
raise AttributeError("This class cannot be modified")
|
|
108
|
+
|
|
109
|
+
def __setattr__(cls, *_):
|
|
110
|
+
raise AttributeError("This class cannot be modified")
|
|
111
|
+
|
|
112
|
+
# Class methods for the derived types
|
|
113
|
+
|
|
114
|
+
def __iter__(cls) -> Iterator[str]:
|
|
115
|
+
"""Iterator over enum keys"""
|
|
116
|
+
yield from cls.__entries__ # type: ignore
|
|
117
|
+
|
|
118
|
+
def _disallow(cls):
|
|
119
|
+
"""Disallow instantiation"""
|
|
120
|
+
raise AttributeError("This class may not be instantiated")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Enum(metaclass=EnumType, empty_ok=True):
|
|
124
|
+
"""
|
|
125
|
+
An uninstantiable Enum base type
|
|
126
|
+
Subclasses must provide type-annotated fields with unique values
|
|
127
|
+
Fields may not be prefixed with "__"
|
|
128
|
+
Derived classes will not be modifiable
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def entries(enum: Any) -> dict[str, tuple[type | None, Any]]:
|
|
133
|
+
"""
|
|
134
|
+
:param enum: A class whose metaclass is EnumType
|
|
135
|
+
:return: A dict mapping names to (type annotation, value) pairs
|
|
136
|
+
If enum's type is not an EnumType, raises TypeError
|
|
137
|
+
"""
|
|
138
|
+
if not isinstance(enum, EnumType):
|
|
139
|
+
raise TypeError("enum must have a metaclass of EnumType")
|
|
140
|
+
return dict(enum.__entries__) # type: ignore[attr-defined]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def values(enum: Any) -> tuple[Any, ...]:
|
|
144
|
+
"""
|
|
145
|
+
:param enum: A class whose metaclass is EnumType
|
|
146
|
+
:return: A tuple of all values of enum
|
|
147
|
+
If enum's type is not an EnumType, raises TypeError
|
|
148
|
+
"""
|
|
149
|
+
if not isinstance(enum, EnumType):
|
|
150
|
+
raise TypeError("enum must have a metaclass of EnumType")
|
|
151
|
+
return tuple(k[1] for k in enum.__entries__.values()) # type: ignore[attr-defined]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
__all__ = ("Enum", "EnumType", "entries", "values", "auto")
|
|
@@ -14,40 +14,53 @@ _FN_ATTRS = (
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Freezable:
|
|
17
|
-
"""
|
|
18
|
-
A base class for objects that can be frozen
|
|
19
|
-
"""
|
|
17
|
+
"""A base class for objects that can be frozen"""
|
|
20
18
|
|
|
21
19
|
def __init__(self, *args, **kwargs) -> None:
|
|
20
|
+
# Avoid name mangling b/c we use object.__setattr__ etc. on these
|
|
21
|
+
# Instead use unlikely to collide names for member variables
|
|
22
22
|
super().__init__(*args, **kwargs) # For MRO super classes
|
|
23
|
-
if hasattr(self, "
|
|
24
|
-
raise ValueError("Base class defines
|
|
25
|
-
self.
|
|
26
|
-
self.
|
|
23
|
+
if hasattr(self, "_freezable_frozen_") or hasattr(self, "_freezable_can_thaw_"):
|
|
24
|
+
raise ValueError("Base class defines _freezable_frozen_ or _freezable_can_thaw_")
|
|
25
|
+
self._freezable_can_thaw_: bool = True
|
|
26
|
+
self._freezable_frozen_: bool = False
|
|
27
|
+
|
|
28
|
+
# Public
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def frozen(self) -> bool:
|
|
32
|
+
"""Check if this object is frozen"""
|
|
33
|
+
return self._freezable_frozen_
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def thawable(self) -> bool:
|
|
37
|
+
"""Check if this object can be thawed or is unfrozen"""
|
|
38
|
+
return self._freezable_can_thaw_
|
|
27
39
|
|
|
28
40
|
def freeze(self, *, permanent: bool = False):
|
|
29
41
|
"""
|
|
30
42
|
Prevent further modifications to this object
|
|
43
|
+
This function can be called on an already frozen object
|
|
31
44
|
"""
|
|
32
45
|
if permanent:
|
|
33
|
-
self
|
|
34
|
-
self
|
|
46
|
+
object.__setattr__(self, "_freezable_can_thaw_", False)
|
|
47
|
+
object.__setattr__(self, "_freezable_frozen_", True)
|
|
35
48
|
|
|
36
49
|
def thaw(self):
|
|
37
50
|
"""
|
|
38
51
|
Allow modifications to this object
|
|
39
52
|
"""
|
|
40
|
-
if not self.
|
|
53
|
+
if not self._freezable_can_thaw_:
|
|
41
54
|
raise RuntimeError("Cannot thaw permanently frozen object")
|
|
42
|
-
object.__setattr__(self, "
|
|
55
|
+
object.__setattr__(self, "_freezable_frozen_", False)
|
|
43
56
|
|
|
44
57
|
def __setattr__(self, key: str, value) -> None:
|
|
45
|
-
if getattr(self, "
|
|
58
|
+
if getattr(self, "_freezable_frozen_", False):
|
|
46
59
|
raise AttributeError("Cannot modify frozen object")
|
|
47
60
|
super().__setattr__(key, value)
|
|
48
61
|
|
|
49
62
|
def __delattr__(self, item: str) -> None:
|
|
50
|
-
if getattr(self, "
|
|
63
|
+
if getattr(self, "_freezable_frozen_", False): # B/c invoked before __init__ defines _frozen
|
|
51
64
|
raise AttributeError("Cannot modify frozen object")
|
|
52
65
|
super().__delattr__(item)
|
|
53
66
|
|
|
@@ -58,12 +71,13 @@ def frozen(arg: str | type):
|
|
|
58
71
|
If passed a string, will freeze after the method with that name
|
|
59
72
|
"""
|
|
60
73
|
|
|
61
|
-
def shim_method(cls: type, name, new) -> None:
|
|
74
|
+
def shim_method(cls: type, name: str, new: Callable) -> None:
|
|
75
|
+
"""Install shims for _FN_ATTRS; modifies new"""
|
|
62
76
|
original = getattr(cls, name)
|
|
63
|
-
for i in _FN_ATTRS:
|
|
77
|
+
for i in _FN_ATTRS: # Copy attributes of old fn onto new fn
|
|
64
78
|
if hasattr(original, i):
|
|
65
79
|
setattr(new, i, getattr(original, i))
|
|
66
|
-
setattr(cls, name, new)
|
|
80
|
+
setattr(cls, name, new) # Install
|
|
67
81
|
|
|
68
82
|
def mk_frozen(cls: type, method: str) -> type:
|
|
69
83
|
original_method: Callable = getattr(cls, method)
|
|
@@ -73,16 +87,16 @@ def frozen(arg: str | type):
|
|
|
73
87
|
def new_method(self, *args, **kwargs):
|
|
74
88
|
ret = original_method(self, *args, **kwargs)
|
|
75
89
|
# pylint: disable=protected-access
|
|
76
|
-
self.
|
|
90
|
+
self._freezable_frozen_ = True # type: ignore[attr-defined]
|
|
77
91
|
return ret
|
|
78
92
|
|
|
79
93
|
def __setattr__(self, key: str, value):
|
|
80
|
-
if getattr(self, "
|
|
94
|
+
if getattr(self, "_freezable_frozen_", False):
|
|
81
95
|
raise AttributeError("Cannot modify frozen object")
|
|
82
96
|
return original_setattr(self, key, value)
|
|
83
97
|
|
|
84
98
|
def __delattr__(self, item: str):
|
|
85
|
-
if getattr(self, "
|
|
99
|
+
if getattr(self, "_freezable_frozen_", False):
|
|
86
100
|
raise AttributeError("Cannot modify frozen object")
|
|
87
101
|
return original_delattr(self, item)
|
|
88
102
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import TypeVar, Self, Any, cast
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from threading import RLock
|
|
4
|
+
from functools import cache
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _None:
|
|
11
|
+
"""A unique NoneType"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NoInstanceError(TypeError):
|
|
15
|
+
"""A TypeError raised when trying to get a Singleton instance before constructing it"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _no_subclass(*_, **__):
|
|
19
|
+
raise TypeError("Singleton subclasses may not be subclassed")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _SingletonType(type):
|
|
23
|
+
"""
|
|
24
|
+
A thread-safe singleton metaclass
|
|
25
|
+
Singleton types may not be subclassed except for the base Singleton type
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_disallow_init_subclass = False # If True, disallow __init_subclass__ in __new__'s attrs
|
|
29
|
+
_instances: dict[type, Any] = {} # Fully constructed and initialized singleton types
|
|
30
|
+
# Lock is preferred to RLock, but could deadlock if user write a constructor that invokes itself
|
|
31
|
+
_cls_locks: dict[type, RLock] = defaultdict(RLock)
|
|
32
|
+
_lock = RLock()
|
|
33
|
+
|
|
34
|
+
def __new__(mcs, name, bases, attrs, **kwargs):
|
|
35
|
+
"""Define a new Singleton type"""
|
|
36
|
+
ret = super().__new__(mcs, name, bases, attrs, **kwargs)
|
|
37
|
+
if mcs._disallow_init_subclass and "__init_subclass__" in attrs:
|
|
38
|
+
raise TypeError("Singleton subclasses may not define __init_subclass__")
|
|
39
|
+
with mcs._lock:
|
|
40
|
+
mcs._cls_locks[ret] = RLock()
|
|
41
|
+
return ret
|
|
42
|
+
|
|
43
|
+
def __call__(cls, *args, **kwargs):
|
|
44
|
+
"""Intercept all instantiations to ensure at most one instance exists"""
|
|
45
|
+
with cls._lock:
|
|
46
|
+
cls_lock = cls._cls_locks[cls]
|
|
47
|
+
with cls_lock:
|
|
48
|
+
if (ret := cls._instances.get(cls, _None)) is _None:
|
|
49
|
+
cls._instances[cls] = (ret := super().__call__(*args, **kwargs))
|
|
50
|
+
return ret
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
@cache
|
|
54
|
+
def instance(mcs, t: type[T]) -> T:
|
|
55
|
+
"""Get the instance of cls"""
|
|
56
|
+
if not isinstance(t, mcs):
|
|
57
|
+
raise TypeError(f"{t} is not a Singleton type")
|
|
58
|
+
with mcs._lock:
|
|
59
|
+
cls_lock = mcs._cls_locks[t]
|
|
60
|
+
with cls_lock:
|
|
61
|
+
if (got := mcs._instances.get(t, _None)) is _None:
|
|
62
|
+
raise NoInstanceError(f"No instance of {t} found")
|
|
63
|
+
return got
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Singleton(metaclass=_SingletonType):
|
|
67
|
+
"""A thread-safe singleton base class"""
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def instance(cls) -> Self:
|
|
71
|
+
"""Blocking get the instance of cls; raise if not constructed"""
|
|
72
|
+
return cast(_SingletonType, type(cls)).instance(cls)
|
|
73
|
+
|
|
74
|
+
def __init_subclass__(cls, *args, **kwargs):
|
|
75
|
+
"""Disallow subclassing"""
|
|
76
|
+
cls.__init_subclass__ = _no_subclass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Disallow __init_subclass__ in Singleton subclasses
|
|
80
|
+
_SingletonType._disallow_init_subclass = True # pylint: disable=protected-access
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: zstdlib
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A set of useful python utilities
|
|
5
5
|
License: GPLv3
|
|
6
6
|
Project-URL: Homepage, https://github.com/zwimer/zstdlib
|
|
@@ -12,6 +12,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
15
16
|
|
|
16
17
|
# zstdlib
|
|
17
18
|
A set of useful python utilities
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,unused-variable
|
|
2
|
-
import unittest
|
|
3
|
-
|
|
4
|
-
from zstdlib.enum 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(TypeError):
|
|
67
|
-
ET10()
|
|
68
|
-
with self.assertRaises(TypeError):
|
|
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(TypeError):
|
|
131
|
-
E10()
|
|
132
|
-
with self.assertRaises(TypeError):
|
|
133
|
-
E10.__init__({})
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if __name__ == "__main__":
|
|
137
|
-
unittest.main()
|
|
@@ -1,126 +0,0 @@
|
|
|
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.singleton 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(TypeError):
|
|
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(TypeError):
|
|
120
|
-
|
|
121
|
-
class S4(S3):
|
|
122
|
-
pass
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if __name__ == "__main__":
|
|
126
|
-
unittest.main()
|
zstdlib-0.0.11/zstdlib/enum.py
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import collections
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class EnumType(type):
|
|
5
|
-
"""
|
|
6
|
-
Metaclass for uninstantiable Enum classes with required annotations and unique values
|
|
7
|
-
Enum values may not be prefixed with _
|
|
8
|
-
These 'Enum' classes may not be modified after creation
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
def __new__(mcs, name, bases, attrs, **kwargs):
|
|
12
|
-
# Disallow instantiation
|
|
13
|
-
for bad in ("__init__", "__new__"):
|
|
14
|
-
if bad in attrs:
|
|
15
|
-
raise AttributeError("Cannot define __init__ or __new__")
|
|
16
|
-
attrs[bad] = None
|
|
17
|
-
# Check annotations
|
|
18
|
-
public = {i: k for i, k in attrs.items() if not i.startswith("__")}
|
|
19
|
-
annotations = attrs.get("__annotations__", {})
|
|
20
|
-
eok = kwargs.pop("empty_ok", False)
|
|
21
|
-
if not public and not annotations:
|
|
22
|
-
if not eok:
|
|
23
|
-
raise ValueError("Enum type is empty")
|
|
24
|
-
if bad := (pub_set := set(public)) - (an_set := set(annotations)):
|
|
25
|
-
raise ValueError(f"All enum entries must be type annotated: {bad}")
|
|
26
|
-
if bad := an_set - pub_set:
|
|
27
|
-
raise ValueError(f"All type annotated entries must have a value: {bad}")
|
|
28
|
-
for i, typ in annotations.items():
|
|
29
|
-
if not isinstance(attrs[i], typ):
|
|
30
|
-
raise TypeError(f"{i} is not of type {typ}")
|
|
31
|
-
# Disallow duplicate values
|
|
32
|
-
counts = collections.Counter(public.values())
|
|
33
|
-
if dups := {i: k for i, k in public.items() if counts[k] > 1}:
|
|
34
|
-
raise ValueError(f"Duplicate values: {dups}")
|
|
35
|
-
# Construct class
|
|
36
|
-
return type.__new__(mcs, name, bases, attrs, **kwargs)
|
|
37
|
-
|
|
38
|
-
# Disallow modification
|
|
39
|
-
|
|
40
|
-
def __delattr__(cls, *_):
|
|
41
|
-
raise AttributeError("This class cannot be modified")
|
|
42
|
-
|
|
43
|
-
def __setattr__(cls, *_):
|
|
44
|
-
raise AttributeError("This class cannot be modified")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class Enum(metaclass=EnumType, empty_ok=True):
|
|
48
|
-
"""
|
|
49
|
-
A uninstantiable Enum base type
|
|
50
|
-
Subclasses must provide type-annotated fields with unique values
|
|
51
|
-
Fields may not be prefixed with "_"
|
|
52
|
-
Derived classes will not be modifiable
|
|
53
|
-
"""
|
|
@@ -1,58 +0,0 @@
|
|
|
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] = set() # Types that should not be constructed again
|
|
11
|
-
_instances: dict[type, Any] = {} # Fully constructed and initialized singleton types
|
|
12
|
-
_lock = Condition()
|
|
13
|
-
# For preventing subclassing of Singletons
|
|
14
|
-
_types: list[type] = []
|
|
15
|
-
_types_lock = RLock()
|
|
16
|
-
|
|
17
|
-
def __new__(mcs, name, bases, attrs, **kwargs):
|
|
18
|
-
"""
|
|
19
|
-
Intercept all class definitions to prevent subclassing singleton types except for Singleton
|
|
20
|
-
"""
|
|
21
|
-
if "__init_subclass__" in attrs:
|
|
22
|
-
raise TypeError("Singleton's should not be subclassed or implement __init_subclass__")
|
|
23
|
-
|
|
24
|
-
def _init_subclass(cls):
|
|
25
|
-
with mcs._types_lock:
|
|
26
|
-
if any(issubclass(cls, i) for i in mcs._types if i is not Singleton):
|
|
27
|
-
err = "Do not derive from Singletons other than the base Singleton type"
|
|
28
|
-
raise TypeError(err)
|
|
29
|
-
mcs._types.append(cls)
|
|
30
|
-
|
|
31
|
-
attrs["__init_subclass__"] = _init_subclass
|
|
32
|
-
ret = super().__new__(mcs, name, bases, attrs, **kwargs)
|
|
33
|
-
with mcs._types_lock:
|
|
34
|
-
mcs._types.append(ret)
|
|
35
|
-
return ret
|
|
36
|
-
|
|
37
|
-
def __call__(cls, *args, **kwargs):
|
|
38
|
-
"""
|
|
39
|
-
Intercept all instantiations to ensure at most one instance exists
|
|
40
|
-
"""
|
|
41
|
-
# If the object has been seen before, wait for it to be available then return it
|
|
42
|
-
with cls._lock:
|
|
43
|
-
if cls in cls._seen:
|
|
44
|
-
cls._lock.wait_for(lambda: cls in cls._instances)
|
|
45
|
-
return cls._instances[cls]
|
|
46
|
-
cls._seen.add(cls)
|
|
47
|
-
# The object has not been constructed before, create it outside any lock to avoid delays
|
|
48
|
-
obj = super().__call__(*args, **kwargs)
|
|
49
|
-
with cls._lock:
|
|
50
|
-
cls._instances[cls] = obj
|
|
51
|
-
cls._lock.notify_all() # Notify other threads of the new object
|
|
52
|
-
return obj
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class Singleton(metaclass=SingletonType):
|
|
56
|
-
"""
|
|
57
|
-
A thread-safe singleton base class
|
|
58
|
-
"""
|
|
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
|
|
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
|