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.
Files changed (35) hide show
  1. {zstdlib-0.0.11 → zstdlib-0.2.0}/PKG-INFO +3 -2
  2. zstdlib-0.2.0/tests/test_enum.py +133 -0
  3. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/test_frozen.py +32 -14
  4. zstdlib-0.2.0/tests/test_singleton.py +139 -0
  5. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/__init__.py +2 -2
  6. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/ansi.py +1 -1
  7. zstdlib-0.2.0/zstdlib/enum.py +154 -0
  8. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/frozen.py +33 -19
  9. zstdlib-0.2.0/zstdlib/singleton.py +80 -0
  10. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib.egg-info/PKG-INFO +3 -2
  11. zstdlib-0.0.11/tests/test_enum.py +0 -137
  12. zstdlib-0.0.11/tests/test_singleton.py +0 -126
  13. zstdlib-0.0.11/zstdlib/enum.py +0 -53
  14. zstdlib-0.0.11/zstdlib/singleton.py +0 -58
  15. {zstdlib-0.0.11 → zstdlib-0.2.0}/LICENSE +0 -0
  16. {zstdlib-0.0.11 → zstdlib-0.2.0}/README.md +0 -0
  17. {zstdlib-0.0.11 → zstdlib-0.2.0}/pyproject.toml +0 -0
  18. {zstdlib-0.0.11 → zstdlib-0.2.0}/setup.cfg +0 -0
  19. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/__init__.py +0 -0
  20. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/log/__init__.py +0 -0
  21. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/log/base.py +0 -0
  22. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/log/test_cute.py +0 -0
  23. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/log/test_trace.py +0 -0
  24. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/not_set.py +0 -0
  25. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/test_ansi.py +0 -0
  26. {zstdlib-0.0.11 → zstdlib-0.2.0}/tests/test_io.py +0 -0
  27. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/io.py +0 -0
  28. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/log/__init__.py +0 -0
  29. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/log/cute.py +0 -0
  30. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/log/trace.py +0 -0
  31. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/not_set.py +0 -0
  32. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib/py.typed +0 -0
  33. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib.egg-info/SOURCES.txt +0 -0
  34. {zstdlib-0.0.11 → zstdlib-0.2.0}/zstdlib.egg-info/dependency_links.txt +0 -0
  35. {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
1
+ Metadata-Version: 2.4
2
2
  Name: zstdlib
3
- Version: 0.0.11
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 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,7 +1,7 @@
1
- __version__ = "0.0.11"
1
+ __version__ = "0.2.0"
2
2
 
3
3
  from .not_set import NotSetType, NotSet
4
4
  from .frozen import Freezable, frozen
5
5
  from .singleton import Singleton
6
- from .enum import Enum
6
+ from .enum import Enum, auto
7
7
  from . import log, io
@@ -144,7 +144,7 @@ def _generate_code(
144
144
  ints.append(foreground.value)
145
145
  if background:
146
146
  ints.append(background.value)
147
- return f"{_PREFIX}{';'.join(str(i) for i in ints)}m"
147
+ return f"{_PREFIX}{';'.join(map(str, ints))}m"
148
148
 
149
149
 
150
150
  @cache
@@ -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, "_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.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()
@@ -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