zstdlib 0.0.9__tar.gz → 0.1.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.9 → zstdlib-0.1.0}/PKG-INFO +5 -4
- {zstdlib-0.0.9 → zstdlib-0.1.0}/pyproject.toml +3 -3
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/test_enum.py +4 -4
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/test_frozen.py +32 -14
- zstdlib-0.1.0/tests/test_io.py +90 -0
- zstdlib-0.1.0/tests/test_singleton.py +139 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/__init__.py +2 -2
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/enum.py +1 -5
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/frozen.py +33 -19
- zstdlib-0.1.0/zstdlib/io.py +238 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/log/trace.py +1 -3
- zstdlib-0.1.0/zstdlib/singleton.py +80 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib.egg-info/PKG-INFO +5 -4
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib.egg-info/SOURCES.txt +2 -0
- zstdlib-0.0.9/tests/test_singleton.py +0 -126
- zstdlib-0.0.9/zstdlib/singleton.py +0 -58
- {zstdlib-0.0.9 → zstdlib-0.1.0}/LICENSE +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/README.md +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/setup.cfg +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/__init__.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/log/__init__.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/log/base.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/log/test_cute.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/log/test_trace.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/not_set.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/test_ansi.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/ansi.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/log/__init__.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/log/cute.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/not_set.py +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/py.typed +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib.egg-info/dependency_links.txt +0 -0
- {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib.egg-info/top_level.txt +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: zstdlib
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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 = ["
|
|
66
|
+
target-version = ["py312", "py313"]
|
|
67
67
|
|
|
68
68
|
[tool.ruff]
|
|
69
69
|
line-length = 110
|
|
@@ -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(
|
|
66
|
+
with self.assertRaises(TypeError):
|
|
67
67
|
ET10()
|
|
68
|
-
with self.assertRaises(
|
|
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(
|
|
130
|
+
with self.assertRaises(TypeError):
|
|
131
131
|
E10()
|
|
132
|
-
with self.assertRaises(
|
|
132
|
+
with self.assertRaises(TypeError):
|
|
133
133
|
E10.__init__({})
|
|
134
134
|
|
|
135
135
|
|
|
@@ -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,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()
|
|
@@ -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()
|
|
@@ -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] =
|
|
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__", {})
|
|
@@ -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,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)
|
|
@@ -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
|
"""
|
|
@@ -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,17 +1,18 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: zstdlib
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
@@ -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(NotImplementedError):
|
|
27
|
-
|
|
28
|
-
class ST4(ST3):
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
def test_multi_thread(self):
|
|
32
|
-
"""
|
|
33
|
-
Ensure that SingletonType is thread safe and that constructing an object doesn't delay other threads
|
|
34
|
-
Technically this is more of a heuristic, but it failing is extremely unlikely
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
class ST5(metaclass=SingletonType):
|
|
38
|
-
def __init__(self):
|
|
39
|
-
sleep(0.2)
|
|
40
|
-
|
|
41
|
-
class ST6(metaclass=SingletonType):
|
|
42
|
-
def __init__(self):
|
|
43
|
-
sleep(0.8)
|
|
44
|
-
|
|
45
|
-
lock = Lock()
|
|
46
|
-
events = []
|
|
47
|
-
results = []
|
|
48
|
-
|
|
49
|
-
def t1() -> None:
|
|
50
|
-
"""
|
|
51
|
-
Construct an ST6 immediately
|
|
52
|
-
"""
|
|
53
|
-
with lock:
|
|
54
|
-
pass
|
|
55
|
-
events.append("START: ST6()")
|
|
56
|
-
results.append(ST6())
|
|
57
|
-
events.append("END: ST6()")
|
|
58
|
-
|
|
59
|
-
def t2() -> None:
|
|
60
|
-
"""
|
|
61
|
-
Construct an ST6 after the first ST6 has started construction but before it has finished
|
|
62
|
-
"""
|
|
63
|
-
with lock:
|
|
64
|
-
pass
|
|
65
|
-
sleep(0.2)
|
|
66
|
-
events.append("START: ST6()")
|
|
67
|
-
results.append(ST6())
|
|
68
|
-
events.append("END: ST6()")
|
|
69
|
-
|
|
70
|
-
def t3() -> None:
|
|
71
|
-
"""
|
|
72
|
-
Construct ST5's after both ST6s have started construction, finishing before either end
|
|
73
|
-
"""
|
|
74
|
-
with lock:
|
|
75
|
-
pass
|
|
76
|
-
sleep(0.4)
|
|
77
|
-
# Loop Enough times that ST6 wil be complete if it ST5 actually constructed each time
|
|
78
|
-
for i in range(10):
|
|
79
|
-
events.append("START: ST5()")
|
|
80
|
-
results.append(ST5())
|
|
81
|
-
events.append("END: ST5()")
|
|
82
|
-
|
|
83
|
-
threads = (Thread(target=t1), Thread(target=t2), Thread(target=t3))
|
|
84
|
-
with lock:
|
|
85
|
-
for i in threads:
|
|
86
|
-
i.start()
|
|
87
|
-
# Give threads a moment to construct then let them go
|
|
88
|
-
sleep(0.2)
|
|
89
|
-
for i in threads:
|
|
90
|
-
i.join()
|
|
91
|
-
# Check results
|
|
92
|
-
wanted = ["START: ST6()"] * 2 + ["START: ST5()", "END: ST5()"] * 10 + ["END: ST6()"] * 2
|
|
93
|
-
self.assertEqual(events, wanted)
|
|
94
|
-
# Check constructed objects
|
|
95
|
-
self.assertEqual(len(results), 2 + 10)
|
|
96
|
-
for i in range(9):
|
|
97
|
-
self.assertIs(results[0], results[i + 1])
|
|
98
|
-
self.assertIsNot(results[0], results[-1])
|
|
99
|
-
self.assertIs(results[-1], results[-2])
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class TestSingleton(unittest.TestCase):
|
|
103
|
-
|
|
104
|
-
def test_valid(self):
|
|
105
|
-
class S1(Singleton):
|
|
106
|
-
pass
|
|
107
|
-
|
|
108
|
-
self.assertIs(S1(), S1())
|
|
109
|
-
|
|
110
|
-
class S2(Singleton):
|
|
111
|
-
pass
|
|
112
|
-
|
|
113
|
-
self.assertIsNot(S1(), S2())
|
|
114
|
-
|
|
115
|
-
def test_subclass(self):
|
|
116
|
-
class S3(Singleton):
|
|
117
|
-
pass
|
|
118
|
-
|
|
119
|
-
with self.assertRaises(NotImplementedError):
|
|
120
|
-
|
|
121
|
-
class S4(S3):
|
|
122
|
-
pass
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if __name__ == "__main__":
|
|
126
|
-
unittest.main()
|
|
@@ -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 NotImplementedError("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 NotImplementedError(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
|