zstdlib 0.7.0__tar.gz → 0.8.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 (36) hide show
  1. {zstdlib-0.7.0 → zstdlib-0.8.0}/PKG-INFO +2 -1
  2. {zstdlib-0.7.0 → zstdlib-0.8.0}/pyproject.toml +5 -2
  3. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/log/base.py +2 -1
  4. zstdlib-0.8.0/tests/test_lazy.py +25 -0
  5. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/__init__.py +4 -2
  6. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/ansi.py +20 -13
  7. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/enum.py +2 -4
  8. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/io.py +1 -1
  9. zstdlib-0.8.0/zstdlib/lazy.py +31 -0
  10. zstdlib-0.8.0/zstdlib/macos.py +198 -0
  11. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib.egg-info/PKG-INFO +2 -1
  12. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib.egg-info/SOURCES.txt +4 -0
  13. zstdlib-0.8.0/zstdlib.egg-info/requires.txt +1 -0
  14. {zstdlib-0.7.0 → zstdlib-0.8.0}/LICENSE +0 -0
  15. {zstdlib-0.7.0 → zstdlib-0.8.0}/README.md +0 -0
  16. {zstdlib-0.7.0 → zstdlib-0.8.0}/setup.cfg +0 -0
  17. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/__init__.py +0 -0
  18. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/log/__init__.py +0 -0
  19. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/log/test_cute.py +0 -0
  20. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/log/test_trace.py +0 -0
  21. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/test_ansi.py +0 -0
  22. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/test_enum.py +0 -0
  23. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/test_frozen.py +0 -0
  24. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/test_io.py +0 -0
  25. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/test_not_set.py +0 -0
  26. {zstdlib-0.7.0 → zstdlib-0.8.0}/tests/test_singleton.py +0 -0
  27. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/frozen.py +0 -0
  28. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/log/__init__.py +0 -0
  29. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/log/cute.py +0 -0
  30. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/log/trace.py +0 -0
  31. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/not_set.py +0 -0
  32. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/py.typed +0 -0
  33. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/signal_cm.py +0 -0
  34. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib/singleton.py +0 -0
  35. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib.egg-info/dependency_links.txt +0 -0
  36. {zstdlib-0.7.0 → zstdlib-0.8.0}/zstdlib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zstdlib
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: A set of useful python utilities
5
5
  License: GPLv3
6
6
  Project-URL: Homepage, https://github.com/zwimer/zstdlib
@@ -11,6 +11,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
11
  Requires-Python: >=3.14
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
+ Requires-Dist: osascript2
14
15
  Dynamic: license-file
15
16
 
16
17
  # zstdlib
@@ -15,6 +15,9 @@ description = "A set of useful python utilities"
15
15
  urls = {Homepage = "https://github.com/zwimer/zstdlib"}
16
16
  requires-python = ">= 3.14"
17
17
  dynamic = ["version"]
18
+ dependencies = [
19
+ "osascript2" # For macOS
20
+ ]
18
21
 
19
22
  [project.readme]
20
23
  file = "README.md"
@@ -60,7 +63,7 @@ ignore=[
60
63
  "E731",
61
64
  "I001",
62
65
  "PLR0913", "PLR2004",
63
- "S101",
66
+ "S101", "S603",
64
67
  "TID252",
65
68
  ]
66
69
  extend-select = [
@@ -87,7 +90,7 @@ convention = "google"
87
90
  "tests/*" = ["DTZ005", "PGH003", "PLW0108"]
88
91
 
89
92
  [tool.bandit]
90
- skips = ["B101", "B104", "B201"]
93
+ skips = ["B101", "B104", "B201", "B404", "B603"]
91
94
 
92
95
  [tool.vulture]
93
96
  ignore_names = []
@@ -29,7 +29,8 @@ class LeftBase:
29
29
  def hijack(
30
30
  cls, name: str, reuse: bool = False, fmt: logging.Formatter | None = None
31
31
  ) -> AbstractContextManager[logging.Logger]:
32
- """
32
+ """Create a hijacked logger
33
+
33
34
  Returns:
34
35
  A logger configured to write to an internal queue
35
36
  """
@@ -0,0 +1,25 @@
1
+ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
2
+ import unittest
3
+
4
+ from zstdlib.lazy import Str
5
+
6
+
7
+ def _append(buf: list[str], *, obj: str) -> str:
8
+ buf.append(obj)
9
+ return str(len(buf))
10
+
11
+
12
+ class TestLazy(unittest.TestCase):
13
+
14
+ def test_str(self) -> None:
15
+ buf: list[str] = []
16
+ obj = "apples"
17
+ x = Str(_append, buf, obj=obj)
18
+ self.assertEqual(buf, [])
19
+ one = str(x) # Invoke Lazy Str
20
+ self.assertEqual(one, "1")
21
+ self.assertEqual(buf, [obj])
22
+
23
+
24
+ if __name__ == "__main__":
25
+ unittest.main()
@@ -1,11 +1,11 @@
1
- __version__ = "0.7.0"
1
+ __version__ = "0.8.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 .signal_cm import signal_cm
7
7
  from .enum import Enum, auto
8
- from . import log, io
8
+ from . import macos, lazy, log, io
9
9
 
10
10
  __all__ = (
11
11
  "Enum",
@@ -16,6 +16,8 @@ __all__ = (
16
16
  "auto",
17
17
  "frozen",
18
18
  "io",
19
+ "lazy",
19
20
  "log",
21
+ "macos",
20
22
  "signal_cm",
21
23
  )
@@ -117,14 +117,18 @@ class Color(metaclass=_ColorMeta):
117
117
  self.code = RawColor._parse_color(fmt) if isinstance(fmt, str) else fmt.code
118
118
 
119
119
  def __call__(self, string: str) -> str:
120
- """
120
+ """Args:
121
+ string: The string to color
122
+
121
123
  Returns:
122
124
  The string color codes with the given ansi code
123
125
  """
124
126
  return f"{self.code}{string}{self.RESET}"
125
127
 
126
128
  def __add__(self, other: Self) -> Self:
127
- """
129
+ """Args:
130
+ other: The other string to color
131
+
128
132
  Returns:
129
133
  A new color made by concatenating these two
130
134
  """
@@ -133,31 +137,30 @@ class Color(metaclass=_ColorMeta):
133
137
  return type(self)(code=f"{self.code[:-1]};{other.code[len(_PREFIX):]}")
134
138
 
135
139
  def __eq__(self, other: object) -> bool:
136
- """
140
+ """Args:
141
+ other: The color to compare against
142
+
137
143
  Returns:
138
144
  Whether the two colors are equal
139
145
  """
140
146
  return isinstance(other, type(self)) and self.code == other.code
141
147
 
142
148
  def __hash__(self) -> int:
143
- """
144
- Returns:
145
- A hash of the current color
146
- """
147
149
  return hash((type(self), self.code))
148
150
 
149
151
  def __repr__(self) -> str:
150
- """
151
- Returns:
152
- The ansi color code this object represents
153
- """
152
+ """Get the ansi color code this object represents"""
154
153
  return f"<Color {self.code!r}>"
155
154
 
156
155
  @classmethod
157
156
  def factory(
158
157
  cls, *, foreground: RawColor | None = None, background: RawColor | None = None, **modifiers: bool
159
158
  ) -> Self:
160
- """
159
+ """Args:
160
+ foreground: The foreground color
161
+ background: The background color
162
+ **modifiers: Color modifiers to apply
163
+
161
164
  Returns:
162
165
  The Ansi given the chosen color and various modifiers
163
166
  """
@@ -167,7 +170,11 @@ class Color(metaclass=_ColorMeta):
167
170
  def _generate_code(
168
171
  foreground: RawColor | None, background: RawColor | None, modifiers: dict[str, bool]
169
172
  ) -> str:
170
- """
173
+ """Args:
174
+ foreground: The foreground color
175
+ background: The background color
176
+ **modifiers: Color modifiers to apply
177
+
171
178
  Returns:
172
179
  The Ansi given the chosen color and various modifiers
173
180
  """
@@ -136,8 +136,7 @@ class Enum(metaclass=EnumType, empty_ok=True):
136
136
 
137
137
 
138
138
  def entries(enum: EnumType) -> dict[str, tuple[type | None, Any]]:
139
- """
140
- Args:
139
+ """Args:
141
140
  enum: A class whose metaclass is EnumType
142
141
 
143
142
  Returns:
@@ -150,8 +149,7 @@ def entries(enum: EnumType) -> dict[str, tuple[type | None, Any]]:
150
149
 
151
150
 
152
151
  def values(enum: Any) -> tuple[Any, ...]:
153
- """
154
- Args:
152
+ """Args:
155
153
  enum: A class whose metaclass is EnumType
156
154
 
157
155
  Returns:
@@ -16,7 +16,7 @@ class ProtectedFile:
16
16
  __slots__ = ("raw",)
17
17
 
18
18
  def __init__(self, raw: IOBase) -> None:
19
- """
19
+ """Create a ProtectedFile
20
20
  Args:
21
21
  raw: The raw IO object to wrap, users can access it directly if they must
22
22
  """
@@ -0,0 +1,31 @@
1
+ from collections.abc import Callable
2
+ from typing import ParamSpec
3
+ from threading import Lock
4
+
5
+ P = ParamSpec("P")
6
+
7
+ __all__ = ("Str",)
8
+
9
+
10
+ class Str:
11
+ """A lazily evaluated string class"""
12
+
13
+ __slots__ = ("_args", "_fn", "_kwargs", "_lock", "_result")
14
+
15
+ def __init__(self, fn: Callable[P, str], *args: P.args, **kwargs: P.kwargs) -> None:
16
+ """Store a function and its arguments, on str(self) return fn(*args, **kwargs)
17
+ The result of the function invocation is cached and will be reused
18
+ """
19
+ self._result: str | None = None
20
+ self._lock = Lock()
21
+ self._fn = fn
22
+ self._args = args
23
+ self._kwargs = kwargs
24
+
25
+ def __str__(self) -> str:
26
+ """Evaluate the saved function"""
27
+ if self._result is not None:
28
+ return self._result
29
+ with self._lock:
30
+ self._result = self._fn(*self._args, **self._kwargs)
31
+ return self._result
@@ -0,0 +1,198 @@
1
+ from typing import NamedTuple, ParamSpec, ClassVar, TypeVar, Self
2
+ from subprocess import TimeoutExpired, Popen
3
+ from collections.abc import Callable
4
+ from multiprocessing import Process
5
+ from logging import getLogger
6
+ from functools import wraps
7
+ from threading import Lock
8
+ from pathlib import Path
9
+ from os import getpid
10
+ import atexit
11
+
12
+ import osascript
13
+
14
+ CAFFEINATE = "/usr/bin/caffeinate" # This is a hard-coded location on macOS
15
+ P = ParamSpec("P")
16
+ R = TypeVar("R")
17
+ _log = getLogger(__name__)
18
+
19
+
20
+ __all__ = ("Caffeinate", "osascript")
21
+
22
+
23
+ class Caffeinate:
24
+ """A small wrapper class around macOS' CLI tool Caffeinate"""
25
+
26
+ _armed_lock: ClassVar[Lock] = Lock()
27
+ _armed: ClassVar[set[Self]] = set()
28
+ __slots__ = ("_cmd", "_data", "_lock", "_wait")
29
+
30
+ class _Data(NamedTuple):
31
+ owner: int
32
+ proc: Popen
33
+
34
+ # pylint: disable=too-many-arguments
35
+ def __init__(
36
+ self,
37
+ cmd: list[Path | str | None] | None = None,
38
+ *,
39
+ wait_for_cmd: bool = False,
40
+ display: bool = False,
41
+ idle: bool = False,
42
+ disk: bool = False,
43
+ system: bool = False,
44
+ user: bool = False,
45
+ timeout: int | None = None,
46
+ wait: int | Popen | Process | None = None,
47
+ ) -> None:
48
+ """Invokes the macOS Caffeinate CLI tool to prevent sleeping.
49
+ Arguments are passed through to Caffeinate, descriptions are taken from the CLI tool; if they disagree with those found in `man caffeinate`, theirs are more likely to be correct.
50
+ Warning: If cmd is passed, re-invoking __enter__ after an __exit__ will cause Caffeinate to run again and thus the command will run again
51
+
52
+ Args:
53
+ cmd: An optional CLI command (and arguments) to run under the Caffeinate process
54
+ wait_for_cmd: If True: Wait until cmd has finished executing before returning from __enter__
55
+ display: If True: Create an assertion to prevent the display from sleeping.
56
+ idle: If True: Create an assertion to prevent the system from idle sleeping.
57
+ disk: If True: Create an assertion to prevent the disk from idle sleeping.
58
+ system: If True: Create an assertion to prevent the system from sleeping. This assertion is valid only when system is running on AC power.
59
+ user: If True: Create an assertion to declare that user is active. If the display is off, this option turns the display on and prevents the display from going into idle sleep. If a timeout is not specified with '-t' option, then this assertion is taken with a default of 5 second timeout.
60
+ timeout: Specifies the timeout value in seconds for which this assertion has to be valid. The assertion is dropped after the specified timeout. This option conflicts with cmd. Note: this timeout is passed to the Caffeinate CLI command, it is not enforced by python
61
+ with this command.
62
+ wait: Waits for the process with the specified pid to exit. Once the the process exits, the assertion is also released. This option conflicts with cmd.
63
+ """
64
+ if cmd is None:
65
+ if wait_for_cmd:
66
+ raise ValueError("wait_for_cmd requires cmd not be None")
67
+ cmd = []
68
+ elif not cmd:
69
+ raise ValueError("Empty cmd; did you mean to use None?")
70
+ elif timeout is not None:
71
+ raise ValueError("timeout may not be used with cmd")
72
+ elif wait is not None:
73
+ raise ValueError("wait may not be used with cmd")
74
+ else:
75
+ cmd = list(cmd)
76
+ self._lock = Lock()
77
+ self._data: Caffeinate._Data | None = None # Have to hardcode "Caffeinate" for mypy :/
78
+ if isinstance(timeout, float):
79
+ raise TypeError("Float timeout not supported")
80
+ if timeout is not None and timeout < 1:
81
+ raise ValueError("Timeout should be a positive integer")
82
+ wait = wait.pid if isinstance(wait, Popen | Process) else wait
83
+ flags = [
84
+ "-d" if display else None,
85
+ "-i" if idle else None,
86
+ "-m" if disk else None,
87
+ "-s" if system else None,
88
+ "-u" if user else None,
89
+ None if timeout is None else f"-t {timeout}",
90
+ None if wait is None else f"-w {wait}",
91
+ ]
92
+ self._cmd = list(map(str, [CAFFEINATE] + [k for i in flags if i for k in i.split(" ")] + cmd))
93
+ self._wait = wait_for_cmd
94
+
95
+ def __enter__(self) -> Self:
96
+ """Start Caffeinate-ing"""
97
+ with self._lock:
98
+ if self._data is not None:
99
+ raise RuntimeError("This context manager has already been entered")
100
+ self._data = self._Data(getpid(), Popen(self._cmd))
101
+ _log.debug("Created Popen(%s), pid=%s", self._cmd, self._data.proc.pid)
102
+ if self._wait:
103
+ self._data.proc.wait()
104
+ return self
105
+
106
+ def __exit__(self, *_: object) -> None:
107
+ with self._lock:
108
+ if self._data is None:
109
+ raise RuntimeError("Context managernever __enter__'d or already __exit__'d")
110
+ if getpid() != self._data.owner:
111
+ return
112
+ _log.debug("Reaping Caffeinate process: %s", self._data.proc.pid)
113
+ self._data.proc.terminate()
114
+ try:
115
+ self._data.proc.wait(timeout=1)
116
+ except TimeoutExpired:
117
+ _log.error("Caffeinate process didn't exit on .terminate()")
118
+ self._data.proc.kill()
119
+ self._data = None
120
+
121
+ @classmethod
122
+ def decorate(
123
+ cls, cmd: list[Path | str | None] | None = None, **caffeinate_kwargs: object
124
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
125
+ """A decorator to wrap a function with a Caffeinate instance
126
+ Example:
127
+ @decorate(idle=True)
128
+ def foo():
129
+ """
130
+ if cmd and callable(cmd[0]):
131
+ raise ValueError(f"Invalid usage of .decorate; see help({cls.__name__}.decorate)")
132
+
133
+ def outer(f: Callable[P, R]) -> Callable[P, R]:
134
+ @wraps(f)
135
+ def inner(*args: P.args, **kwargs: P.kwargs) -> R:
136
+ with cls(cmd, **caffeinate_kwargs): # type: ignore[arg-type]
137
+ return f(*args, **kwargs)
138
+
139
+ return inner
140
+
141
+ return outer
142
+
143
+ @classmethod
144
+ def new(
145
+ cls,
146
+ cmd: list[Path | str | None] | None = None,
147
+ *,
148
+ survive_exit: bool = False,
149
+ **kwargs: object,
150
+ ) -> Self:
151
+ """Return an __enter__'d Caffeinate, whose __exit__ will run atexit
152
+ cmd and **kwargs are passed to __init__
153
+
154
+ Args:
155
+ cmd: Passed to Caffeinate
156
+ survive_exit: Do not kill this Caffeinate instance when this process exits
157
+ kwargs: Passed to Caffeinate
158
+ """
159
+ ret = cls(cmd, **kwargs) # type: ignore[arg-type]
160
+ _ = ret.__enter__() # pylint: disable=unnecessary-dunder-call
161
+ if not survive_exit:
162
+ with cls._armed_lock:
163
+ cls._armed.add(ret)
164
+ return ret
165
+
166
+ def survive_exit(self) -> None:
167
+ """Disable atexit death; only permitted for instances created by .new()"""
168
+ # pylint: disable=protected-access
169
+ with self._armed_lock:
170
+ if self not in self._armed:
171
+ msg = f"This instance was not created by {self.__class__.__name__}.new() or already called .survive_exit()"
172
+ raise ValueError(msg)
173
+ self._armed.remove(self)
174
+ assert self._data is not None
175
+ _log.debug("Disabled cleanup of caffeinate subprocess PID=%s", self._data.proc.pid)
176
+
177
+ @classmethod
178
+ def _atexit(cls) -> None:
179
+ """Clean up child Caffeinate subprocesses started by this class"""
180
+ # pylint: disable=protected-access
181
+ pid = getpid()
182
+ _log.debug("Cleaning up all owned Caffeinate subprocesses started this process (PID=%s)", pid)
183
+ with cls._armed_lock:
184
+ for i in cls._armed:
185
+ if i._data is None:
186
+ _log.error("Unentered Caffeinate process in ._armed; this should never happen")
187
+ continue
188
+ if i._data.owner == pid:
189
+ i.__exit__()
190
+ cls._armed.clear()
191
+
192
+ def __repr__(self) -> str:
193
+ if self._data is None:
194
+ return f"<Caffeinate: NOT RUNNING, flags={self._cmd[1:]}>"
195
+ return f"<Caffeinate: PID={self._data.proc.pid}, flags={self._cmd[1:]}>"
196
+
197
+
198
+ atexit.register(Caffeinate._atexit) # pylint: disable=protected-access
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zstdlib
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: A set of useful python utilities
5
5
  License: GPLv3
6
6
  Project-URL: Homepage, https://github.com/zwimer/zstdlib
@@ -11,6 +11,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
11
  Requires-Python: >=3.14
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
+ Requires-Dist: osascript2
14
15
  Dynamic: license-file
15
16
 
16
17
  # zstdlib
@@ -6,6 +6,7 @@ tests/test_ansi.py
6
6
  tests/test_enum.py
7
7
  tests/test_frozen.py
8
8
  tests/test_io.py
9
+ tests/test_lazy.py
9
10
  tests/test_not_set.py
10
11
  tests/test_singleton.py
11
12
  tests/log/__init__.py
@@ -17,6 +18,8 @@ zstdlib/ansi.py
17
18
  zstdlib/enum.py
18
19
  zstdlib/frozen.py
19
20
  zstdlib/io.py
21
+ zstdlib/lazy.py
22
+ zstdlib/macos.py
20
23
  zstdlib/not_set.py
21
24
  zstdlib/py.typed
22
25
  zstdlib/signal_cm.py
@@ -24,6 +27,7 @@ zstdlib/singleton.py
24
27
  zstdlib.egg-info/PKG-INFO
25
28
  zstdlib.egg-info/SOURCES.txt
26
29
  zstdlib.egg-info/dependency_links.txt
30
+ zstdlib.egg-info/requires.txt
27
31
  zstdlib.egg-info/top_level.txt
28
32
  zstdlib/log/__init__.py
29
33
  zstdlib/log/cute.py
@@ -0,0 +1 @@
1
+ osascript2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes