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.
Files changed (33) hide show
  1. {zstdlib-0.0.9 → zstdlib-0.1.0}/PKG-INFO +5 -4
  2. {zstdlib-0.0.9 → zstdlib-0.1.0}/pyproject.toml +3 -3
  3. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/test_enum.py +4 -4
  4. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/test_frozen.py +32 -14
  5. zstdlib-0.1.0/tests/test_io.py +90 -0
  6. zstdlib-0.1.0/tests/test_singleton.py +139 -0
  7. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/__init__.py +2 -2
  8. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/enum.py +1 -5
  9. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/frozen.py +33 -19
  10. zstdlib-0.1.0/zstdlib/io.py +238 -0
  11. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/log/trace.py +1 -3
  12. zstdlib-0.1.0/zstdlib/singleton.py +80 -0
  13. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib.egg-info/PKG-INFO +5 -4
  14. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib.egg-info/SOURCES.txt +2 -0
  15. zstdlib-0.0.9/tests/test_singleton.py +0 -126
  16. zstdlib-0.0.9/zstdlib/singleton.py +0 -58
  17. {zstdlib-0.0.9 → zstdlib-0.1.0}/LICENSE +0 -0
  18. {zstdlib-0.0.9 → zstdlib-0.1.0}/README.md +0 -0
  19. {zstdlib-0.0.9 → zstdlib-0.1.0}/setup.cfg +0 -0
  20. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/__init__.py +0 -0
  21. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/log/__init__.py +0 -0
  22. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/log/base.py +0 -0
  23. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/log/test_cute.py +0 -0
  24. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/log/test_trace.py +0 -0
  25. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/not_set.py +0 -0
  26. {zstdlib-0.0.9 → zstdlib-0.1.0}/tests/test_ansi.py +0 -0
  27. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/ansi.py +0 -0
  28. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/log/__init__.py +0 -0
  29. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/log/cute.py +0 -0
  30. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/not_set.py +0 -0
  31. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib/py.typed +0 -0
  32. {zstdlib-0.0.9 → zstdlib-0.1.0}/zstdlib.egg-info/dependency_links.txt +0 -0
  33. {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
1
+ Metadata-Version: 2.4
2
2
  Name: zstdlib
3
- Version: 0.0.9
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.10
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.10
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.10",
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.10"
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 = ["py310", "py311", "py312", "py313"]
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(NotImplementedError):
66
+ with self.assertRaises(TypeError):
67
67
  ET10()
68
- with self.assertRaises(NotImplementedError):
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(NotImplementedError):
130
+ with self.assertRaises(TypeError):
131
131
  E10()
132
- with self.assertRaises(NotImplementedError):
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 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,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,7 +1,7 @@
1
- __version__ = "0.0.9"
1
+ __version__ = "0.1.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
6
  from .enum import Enum
7
- from . import log
7
+ from . import log, io
@@ -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] = _not_implemented
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, "_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,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
1
+ Metadata-Version: 2.4
2
2
  Name: zstdlib
3
- Version: 0.0.9
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.10
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.10
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