pyutilkit 0.8.0__tar.gz → 0.9.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.

Potentially problematic release.


This version of pyutilkit might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyutilkit
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: python's missing batteries
5
5
  Home-page: https://pyutilkit.readthedocs.io/en/stable/
6
6
  License: BSD-3-Clause
@@ -6,7 +6,7 @@ build-backend = "phosphorus.construction.api"
6
6
 
7
7
  [project]
8
8
  name = "pyutilkit"
9
- version = "0.8.0"
9
+ version = "0.9.0"
10
10
 
11
11
  authors = [
12
12
  { name = "Stephanos Kuma", email = "stephanos@kuma.ai" },
@@ -67,7 +67,7 @@ target-version = [
67
67
  check_untyped_defs = true
68
68
  disallow_any_decorated = true
69
69
  disallow_any_explicit = true
70
- disallow_any_expr = true
70
+ disallow_any_expr = false # many builtins are Any
71
71
  disallow_any_generics = true
72
72
  disallow_any_unimported = true
73
73
  disallow_incomplete_defs = true
@@ -102,6 +102,7 @@ select = [
102
102
  "ALL",
103
103
  ]
104
104
  ignore = [
105
+ "C901", # Adding a limit to complexity is too arbitrary
105
106
  "COM812", # Avoid magic trailing commas
106
107
  "D10", # Not everything needs a docstring
107
108
  "D203", # Prefer `no-blank-line-before-class` (D211)
@@ -146,7 +147,7 @@ data_file = ".cov_cache/coverage.dat"
146
147
  exclude_also = [
147
148
  "if TYPE_CHECKING:",
148
149
  ]
149
- fail_under = 75
150
+ fail_under = 90
150
151
  precision = 2
151
152
  show_missing = true
152
153
  skip_covered = true
@@ -34,12 +34,12 @@ def run_command(
34
34
  )
35
35
 
36
36
  for line in process.stdout or []:
37
- sys.stdout.buffer.write(line) # type: ignore[misc]
37
+ sys.stdout.buffer.write(line)
38
38
  sys.stdout.flush()
39
39
  stdout.append(line)
40
40
 
41
41
  for line in process.stderr or []:
42
- sys.stderr.buffer.write(line) # type: ignore[misc]
42
+ sys.stderr.buffer.write(line)
43
43
  sys.stderr.flush()
44
44
  stderr.append(line)
45
45
 
@@ -50,6 +50,6 @@ def run_command(
50
50
  stdout=b"".join(stdout),
51
51
  stderr=b"".join(stderr),
52
52
  pid=process.pid,
53
- returncode=process.returncode, # type: ignore[misc]
53
+ returncode=process.returncode,
54
54
  elapsed=stopwatch.elapsed,
55
55
  )
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import sys
5
+ from dataclasses import dataclass
5
6
  from enum import IntEnum, unique
6
7
  from math import ceil, floor
7
8
  from typing import TYPE_CHECKING
@@ -66,36 +67,50 @@ class SGRCodes(IntEnum):
66
67
  return f"\033[{self.value}m"
67
68
 
68
69
 
69
- class SGRString(str):
70
- _sgr: tuple[SGRCodes, ...]
70
+ @dataclass(frozen=True, order=True)
71
+ class SGRString:
72
+ __slots__ = ( # py3.9: remove this line
73
+ "_force_prefix",
74
+ "_force_sgr",
75
+ "_is_error",
76
+ "_prefix",
77
+ "_sgr",
78
+ "_string",
79
+ "_suffix",
80
+ )
81
+
71
82
  _string: str
83
+ _sgr: tuple[SGRCodes, ...]
72
84
  _prefix: str
73
85
  _suffix: str
74
- __slots__ = ("_prefix", "_sgr", "_string", "_suffix")
86
+ _force_prefix: bool
87
+ _force_sgr: bool
88
+ _is_error: bool
75
89
 
76
- def __new__(
77
- cls,
90
+ def __init__(
91
+ self,
78
92
  obj: object,
79
93
  *,
80
94
  prefix: str = "",
81
95
  suffix: str = "",
82
96
  params: Iterable[SGRCodes] = (),
83
- ) -> Self:
84
- string = super().__new__(cls, obj)
97
+ force_prefix: bool = False,
98
+ force_sgr: bool = False,
99
+ is_error: bool = False,
100
+ ) -> None:
85
101
  params = tuple(params)
86
- object.__setattr__(string, "_prefix", prefix)
87
- object.__setattr__(string, "_string", str(obj))
88
- object.__setattr__(string, "_sgr", params)
89
- object.__setattr__(string, "_suffix", suffix)
90
- return string
91
-
92
- def __setattr__(self, name: str, value: object) -> None:
93
- msg = "SGRString is immutable"
94
- raise AttributeError(msg)
102
+ force_prefix = (
103
+ force_prefix or os.getenv("PY_UTIL_FORCE_PREFIX", "").lower() in TRUE_VAR
104
+ )
105
+ force_sgr = force_sgr or os.getenv("PY_UTIL_FORCE_SGR", "").lower() in TRUE_VAR
95
106
 
96
- def __delattr__(self, name: str) -> None:
97
- msg = "SGRString is immutable"
98
- raise AttributeError(msg)
107
+ object.__setattr__(self, "_prefix", prefix)
108
+ object.__setattr__(self, "_string", str(obj))
109
+ object.__setattr__(self, "_sgr", params)
110
+ object.__setattr__(self, "_suffix", suffix)
111
+ object.__setattr__(self, "_force_prefix", force_prefix)
112
+ object.__setattr__(self, "_force_sgr", force_sgr)
113
+ object.__setattr__(self, "_is_error", is_error)
99
114
 
100
115
  def __str__(self) -> str:
101
116
  sgr_prefix = "".join(code.sequence for code in self._sgr)
@@ -113,6 +128,9 @@ class SGRString(str):
113
128
  prefix=self._prefix,
114
129
  suffix=self._suffix,
115
130
  params=self._sgr,
131
+ force_prefix=self._force_prefix,
132
+ force_sgr=self._force_sgr,
133
+ is_error=self._is_error,
116
134
  )
117
135
 
118
136
  def __rmul__(self, other: object) -> Self:
@@ -123,16 +141,12 @@ class SGRString(str):
123
141
  prefix=self._prefix,
124
142
  suffix=self._suffix,
125
143
  params=self._sgr,
144
+ force_prefix=self._force_prefix,
145
+ force_sgr=self._force_sgr,
146
+ is_error=self._is_error,
126
147
  )
127
148
 
128
- def print(
129
- self,
130
- end: str = "\n",
131
- *,
132
- force_prefix: bool = False,
133
- force_sgr: bool = False,
134
- is_error: bool = False,
135
- ) -> None:
149
+ def print(self, end: str = "\n", *, full_color: bool = False) -> None:
136
150
  """Print the command output.
137
151
 
138
152
  The command will be printed to stdout if it's not the output of an error,
@@ -141,8 +155,8 @@ class SGRString(str):
141
155
  If the output stream isn't a tty, it will strip the SGR codes and the prefix,
142
156
  unless forced to keep them.
143
157
  """
144
- file = sys.stderr if is_error else sys.stdout
145
- if file.isatty(): # type: ignore[misc]
158
+ file = sys.stderr if self._is_error else sys.stdout
159
+ if file.isatty():
146
160
  prefix = self._prefix
147
161
  sgr_prefix = "".join(code.sequence for code in self._sgr)
148
162
  sgr_suffix = SGRCodes.RESET.sequence
@@ -152,16 +166,17 @@ class SGRString(str):
152
166
  sgr_prefix = ""
153
167
  sgr_suffix = ""
154
168
  suffix = ""
155
- if force_sgr or os.getenv("PY_UTIL_FORCE_SGR", "").lower() in TRUE_VAR:
169
+ if self._force_sgr:
156
170
  sgr_prefix = "".join(code.sequence for code in self._sgr)
157
171
  sgr_suffix = SGRCodes.RESET.sequence if self._sgr else ""
158
- if (
159
- force_prefix
160
- or os.getenv("PY_UTIL_FORCE_OUTFIX", "").lower() in TRUE_VAR
161
- ):
172
+ if self._force_prefix:
162
173
  prefix = self._prefix
163
174
  suffix = self._suffix
164
175
 
176
+ if full_color:
177
+ prefix, sgr_prefix = sgr_prefix, prefix
178
+ suffix, sgr_suffix = sgr_suffix, suffix
179
+
165
180
  print(
166
181
  prefix,
167
182
  sgr_prefix,
@@ -180,23 +195,98 @@ class SGRString(str):
180
195
  left_spaces: int = 1,
181
196
  right_spaces: int = 1,
182
197
  space: str = " ",
183
- force_sgr: bool = False,
184
- is_error: bool = False,
185
198
  ) -> None:
186
199
  try:
187
200
  terminal_size = os.get_terminal_size()
188
201
  except OSError:
189
202
  # in pseudo-terminals an OSError is thrown
190
- self.print(force_prefix=True, force_sgr=force_sgr, is_error=is_error)
203
+ self.print()
191
204
  return
192
205
 
193
206
  columns = terminal_size.columns
194
207
  title_length = left_spaces + len(self) + right_spaces
195
208
  if title_length >= columns:
196
- self.print(is_error=is_error)
209
+ self.print()
197
210
  return
198
211
 
199
212
  half = (columns - title_length) / 2
200
213
  prefix = f"{padding * ceil(half)}{space * left_spaces}{self._prefix}"
201
214
  suffix = f"{self._suffix}{space * right_spaces}{padding * floor(half)}"
202
215
  type(self)(self._string, prefix=prefix, suffix=suffix, params=self._sgr).print()
216
+
217
+
218
+ @dataclass(frozen=True, order=True)
219
+ class SGROutput:
220
+ __slots__ = ("_strings",) # py3.9: remove this line
221
+ _strings: tuple[SGRString, ...]
222
+
223
+ def __init__(
224
+ self,
225
+ strings: Iterable[SGRString],
226
+ force_prefix: bool | None = None,
227
+ force_sgr: bool | None = None,
228
+ is_error: bool | None = None,
229
+ ) -> None:
230
+ strings = tuple(
231
+ self._clean_string(
232
+ string,
233
+ force_prefix=force_prefix,
234
+ force_sgr=force_sgr,
235
+ is_error=is_error,
236
+ )
237
+ for string in strings
238
+ )
239
+
240
+ object.__setattr__(self, "_strings", strings)
241
+
242
+ @staticmethod
243
+ def _clean_string(
244
+ string: SGRString,
245
+ force_prefix: bool | None,
246
+ force_sgr: bool | None,
247
+ is_error: bool | None,
248
+ ) -> SGRString:
249
+ force_prefix = (
250
+ string._force_prefix # noqa: SLF001
251
+ if force_prefix is None
252
+ else force_prefix
253
+ )
254
+ force_sgr = (
255
+ string._force_sgr if force_sgr is None else force_sgr # noqa: SLF001
256
+ )
257
+ is_error = string._is_error if is_error is None else is_error # noqa: SLF001
258
+ return SGRString(
259
+ string._string, # noqa: SLF001
260
+ prefix=string._prefix, # noqa: SLF001
261
+ suffix=string._suffix, # noqa: SLF001
262
+ params=string._sgr, # noqa: SLF001
263
+ force_prefix=force_prefix,
264
+ force_sgr=force_sgr,
265
+ is_error=is_error,
266
+ )
267
+
268
+ def print(self, sep: str = "", end: str = "\n") -> None:
269
+ n = len(self._strings)
270
+ for index, string in enumerate(self._strings, start=1):
271
+ current_end = end if index == n else sep
272
+ string.print(end=current_end)
273
+
274
+ def header(
275
+ self,
276
+ *,
277
+ padding: str = " ",
278
+ left_spaces: int = 1,
279
+ right_spaces: int = 1,
280
+ space: str = " ",
281
+ ) -> None:
282
+ n = len(self._strings)
283
+ if n > 1:
284
+ msg = "Only one string is allowed for the header"
285
+ raise ValueError(msg)
286
+
287
+ self._strings[0].header(
288
+ padding=padding,
289
+ left_spaces=left_spaces,
290
+ right_spaces=right_spaces,
291
+ space=space,
292
+ )
@@ -91,7 +91,7 @@ class Timing:
91
91
  def __truediv__(self, other: object) -> Timing:
92
92
  if not isinstance(other, int):
93
93
  return NotImplemented
94
- return Timing(nanoseconds=self.nanoseconds // other)
94
+ return Timing(nanoseconds=round(self.nanoseconds / other))
95
95
 
96
96
 
97
97
  class Stopwatch:
File without changes
File without changes