zstdlib 0.0.11__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.
Files changed (33) hide show
  1. {zstdlib-0.0.11 → zstdlib-0.1.0}/PKG-INFO +3 -2
  2. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/test_frozen.py +32 -14
  3. zstdlib-0.1.0/tests/test_singleton.py +139 -0
  4. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/__init__.py +1 -1
  5. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/frozen.py +33 -19
  6. zstdlib-0.1.0/zstdlib/singleton.py +80 -0
  7. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib.egg-info/PKG-INFO +3 -2
  8. zstdlib-0.0.11/tests/test_singleton.py +0 -126
  9. zstdlib-0.0.11/zstdlib/singleton.py +0 -58
  10. {zstdlib-0.0.11 → zstdlib-0.1.0}/LICENSE +0 -0
  11. {zstdlib-0.0.11 → zstdlib-0.1.0}/README.md +0 -0
  12. {zstdlib-0.0.11 → zstdlib-0.1.0}/pyproject.toml +0 -0
  13. {zstdlib-0.0.11 → zstdlib-0.1.0}/setup.cfg +0 -0
  14. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/__init__.py +0 -0
  15. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/log/__init__.py +0 -0
  16. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/log/base.py +0 -0
  17. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/log/test_cute.py +0 -0
  18. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/log/test_trace.py +0 -0
  19. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/not_set.py +0 -0
  20. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/test_ansi.py +0 -0
  21. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/test_enum.py +0 -0
  22. {zstdlib-0.0.11 → zstdlib-0.1.0}/tests/test_io.py +0 -0
  23. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/ansi.py +0 -0
  24. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/enum.py +0 -0
  25. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/io.py +0 -0
  26. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/log/__init__.py +0 -0
  27. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/log/cute.py +0 -0
  28. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/log/trace.py +0 -0
  29. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/not_set.py +0 -0
  30. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib/py.typed +0 -0
  31. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib.egg-info/SOURCES.txt +0 -0
  32. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib.egg-info/dependency_links.txt +0 -0
  33. {zstdlib-0.0.11 → zstdlib-0.1.0}/zstdlib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: zstdlib
3
- Version: 0.0.11
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
@@ -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
@@ -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 F2:
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
- f2 = F2()
87
- self.assertEqual(f2.__init__.__doc__.strip(), "init doc")
88
- self.assertEqual(f2.__init__.__name__, "__init__")
89
- self.assertEqual(f2.__init__.__qualname__, "TestFrozen.test_metadata.<locals>.F2.__init__")
90
- self.assertTupleEqual(f2.__init__.__defaults__, (1,))
91
- self.assertDictEqual(f2.__init__.__kwdefaults__, {"b": False})
92
- self.assertDictEqual(f2.__init__.__annotations__, {"a": int, "b": bool, "return": None})
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 F3:
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
- f3 = F3()
104
- self.assertEqual(f3.a, 1)
105
- f3.custom()
106
- self.assertEqual(f3.a, 2)
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
- f3.a = 1
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()
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.11"
1
+ __version__ = "0.1.0"
2
2
 
3
3
  from .not_set import NotSetType, NotSet
4
4
  from .frozen import Freezable, frozen
@@ -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, "_frozen") or hasattr(self, "_can_thaw"):
24
- raise ValueError("Base class defines _frozen or _can_thaw")
25
- self._can_thaw: bool = True
26
- self._frozen: bool = False
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._can_thaw = False
34
- self._frozen = True
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._can_thaw:
53
+ if not self._freezable_can_thaw_:
41
54
  raise RuntimeError("Cannot thaw permanently frozen object")
42
- object.__setattr__(self, "_frozen", False)
55
+ object.__setattr__(self, "_freezable_frozen_", False)
43
56
 
44
57
  def __setattr__(self, key: str, value) -> None:
45
- if getattr(self, "_frozen", False):
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, "_frozen", False):
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._frozen = True # type: ignore[attr-defined]
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, "_frozen", False):
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, "_frozen", False):
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
1
+ Metadata-Version: 2.4
2
2
  Name: zstdlib
3
- Version: 0.0.11
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
@@ -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,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()
@@ -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