pyutilkit 0.10.0__tar.gz → 0.11.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.
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/LICENSE.md +1 -1
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/PKG-INFO +13 -10
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/docs/README.md +11 -8
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/pyproject.toml +39 -23
- pyutilkit-0.11.0/src/pyutilkit/__version__.py +1 -0
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/classes.py +8 -5
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/date_utils.py +1 -1
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/files.py +2 -5
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/term.py +4 -13
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/timing.py +52 -17
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/__init__.py +0 -0
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/data/__init__.py +0 -0
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/data/timezones.py +0 -0
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/py.typed +0 -0
- {pyutilkit-0.10.0 → pyutilkit-0.11.0}/src/pyutilkit/subprocess.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyutilkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: python's missing batteries
|
|
5
5
|
Home-page: https://pyutilkit.readthedocs.io/en/stable/
|
|
6
6
|
License: BSD-3-Clause
|
|
7
7
|
Keywords: utils
|
|
8
8
|
Author: Stephanos Kuma
|
|
9
9
|
Author-email: "Stephanos Kuma" <stephanos@kuma.ai>
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: License :: OSI Approved :: BSD License
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -21,11 +21,12 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
[![build][build_badge]][build_url]
|
|
23
23
|
[![lint][lint_badge]][lint_url]
|
|
24
|
-
[![tests][
|
|
24
|
+
[![tests][tests_badge]][tests_url]
|
|
25
25
|
[![license][licence_badge]][licence_url]
|
|
26
|
+
[![codecov][codecov_badge]][codecov_url]
|
|
27
|
+
[![readthedocs][readthedocs_badge]][readthedocs_url]
|
|
26
28
|
[![pypi][pypi_badge]][pypi_url]
|
|
27
29
|
[![downloads][pepy_badge]][pepy_url]
|
|
28
|
-
[![code style: black][black_badge]][black_url]
|
|
29
30
|
[![build automation: yam][yam_badge]][yam_url]
|
|
30
31
|
[![Lint: ruff][ruff_badge]][ruff_url]
|
|
31
32
|
|
|
@@ -37,23 +38,25 @@ hopes to stop this repetition.
|
|
|
37
38
|
|
|
38
39
|
## Links
|
|
39
40
|
|
|
40
|
-
-
|
|
41
|
-
-
|
|
41
|
+
- [Documentation]
|
|
42
|
+
- [Changelog]
|
|
42
43
|
|
|
43
44
|
[build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
|
|
44
45
|
[build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
|
|
45
46
|
[lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
|
|
46
47
|
[lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
|
|
47
|
-
[
|
|
48
|
-
[
|
|
48
|
+
[tests_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
|
|
49
|
+
[tests_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
|
|
49
50
|
[licence_badge]: https://img.shields.io/pypi/l/pyutilkit
|
|
50
51
|
[licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
|
|
52
|
+
[codecov_badge]: https://codecov.io/github/spapanik/pyutilkit/graph/badge.svg?token=Q20F84BW72
|
|
53
|
+
[codecov_url]: https://codecov.io/github/spapanik/pyutilkit
|
|
54
|
+
[readthedocs_badge]: https://readthedocs.org/projects/pyutilkit/badge/?version=latest
|
|
55
|
+
[readthedocs_url]: https://pyutilkit.readthedocs.io/en/latest/
|
|
51
56
|
[pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
|
|
52
57
|
[pypi_url]: https://pypi.org/project/pyutilkit
|
|
53
58
|
[pepy_badge]: https://pepy.tech/badge/pyutilkit
|
|
54
59
|
[pepy_url]: https://pepy.tech/project/pyutilkit
|
|
55
|
-
[black_badge]: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
56
|
-
[black_url]: https://github.com/psf/black
|
|
57
60
|
[yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
|
|
58
61
|
[yam_url]: https://github.com/spapanik/yamk
|
|
59
62
|
[ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[![build][build_badge]][build_url]
|
|
4
4
|
[![lint][lint_badge]][lint_url]
|
|
5
|
-
[![tests][
|
|
5
|
+
[![tests][tests_badge]][tests_url]
|
|
6
6
|
[![license][licence_badge]][licence_url]
|
|
7
|
+
[![codecov][codecov_badge]][codecov_url]
|
|
8
|
+
[![readthedocs][readthedocs_badge]][readthedocs_url]
|
|
7
9
|
[![pypi][pypi_badge]][pypi_url]
|
|
8
10
|
[![downloads][pepy_badge]][pepy_url]
|
|
9
|
-
[![code style: black][black_badge]][black_url]
|
|
10
11
|
[![build automation: yam][yam_badge]][yam_url]
|
|
11
12
|
[![Lint: ruff][ruff_badge]][ruff_url]
|
|
12
13
|
|
|
@@ -18,23 +19,25 @@ hopes to stop this repetition.
|
|
|
18
19
|
|
|
19
20
|
## Links
|
|
20
21
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
22
|
+
- [Documentation]
|
|
23
|
+
- [Changelog]
|
|
23
24
|
|
|
24
25
|
[build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
|
|
25
26
|
[build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
|
|
26
27
|
[lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
|
|
27
28
|
[lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
|
|
28
|
-
[
|
|
29
|
-
[
|
|
29
|
+
[tests_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
|
|
30
|
+
[tests_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
|
|
30
31
|
[licence_badge]: https://img.shields.io/pypi/l/pyutilkit
|
|
31
32
|
[licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
|
|
33
|
+
[codecov_badge]: https://codecov.io/github/spapanik/pyutilkit/graph/badge.svg?token=Q20F84BW72
|
|
34
|
+
[codecov_url]: https://codecov.io/github/spapanik/pyutilkit
|
|
35
|
+
[readthedocs_badge]: https://readthedocs.org/projects/pyutilkit/badge/?version=latest
|
|
36
|
+
[readthedocs_url]: https://pyutilkit.readthedocs.io/en/latest/
|
|
32
37
|
[pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
|
|
33
38
|
[pypi_url]: https://pypi.org/project/pyutilkit
|
|
34
39
|
[pepy_badge]: https://pepy.tech/badge/pyutilkit
|
|
35
40
|
[pepy_url]: https://pepy.tech/project/pyutilkit
|
|
36
|
-
[black_badge]: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
37
|
-
[black_url]: https://github.com/psf/black
|
|
38
41
|
[yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
|
|
39
42
|
[yam_url]: https://github.com/spapanik/yamk
|
|
40
43
|
[ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
requires = [
|
|
3
|
-
"phosphorus>=0.
|
|
3
|
+
"phosphorus>=0.10",
|
|
4
4
|
]
|
|
5
5
|
build-backend = "phosphorus.construction.api"
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pyutilkit"
|
|
9
|
-
|
|
9
|
+
dynamic = [
|
|
10
|
+
"version",
|
|
11
|
+
]
|
|
10
12
|
|
|
11
13
|
authors = [
|
|
12
14
|
{ name = "Stephanos Kuma", email = "stephanos@kuma.ai" },
|
|
@@ -25,7 +27,7 @@ classifiers = [
|
|
|
25
27
|
"License :: OSI Approved :: BSD License",
|
|
26
28
|
]
|
|
27
29
|
|
|
28
|
-
requires-python = ">=3.
|
|
30
|
+
requires-python = ">=3.10"
|
|
29
31
|
dependencies = [
|
|
30
32
|
"tzdata; os_name == 'nt'",
|
|
31
33
|
]
|
|
@@ -35,33 +37,38 @@ homepage = "https://pyutilkit.readthedocs.io/en/stable/"
|
|
|
35
37
|
repository = "https://github.com/spapanik/pyutilkit"
|
|
36
38
|
documentation = "https://pyutilkit.readthedocs.io/en/stable/"
|
|
37
39
|
|
|
38
|
-
[
|
|
40
|
+
[dependency-groups]
|
|
39
41
|
dev = [
|
|
40
42
|
"ipdb~=0.13",
|
|
41
|
-
"ipython~=8.
|
|
43
|
+
"ipython~=8.37",
|
|
44
|
+
"ptpython~=3.0",
|
|
45
|
+
{ include-group = "lint" },
|
|
46
|
+
{ include-group = "test" },
|
|
47
|
+
{ include-group = "docs" },
|
|
42
48
|
]
|
|
43
49
|
lint = [
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
50
|
+
"mypy~=1.18",
|
|
51
|
+
"ruff~=0.13",
|
|
52
|
+
{ include-group = "test_core" },
|
|
47
53
|
]
|
|
48
54
|
test = [
|
|
55
|
+
"pytest-cov~=7.0",
|
|
56
|
+
{ include-group = "test_core" },
|
|
57
|
+
]
|
|
58
|
+
test_core = [
|
|
49
59
|
"freezegun~=1.5",
|
|
50
|
-
"pytest~=
|
|
51
|
-
"pytest-cov~=6.0",
|
|
60
|
+
"pytest~=9.0",
|
|
52
61
|
]
|
|
53
62
|
docs = [
|
|
54
63
|
"mkdocs~=1.6",
|
|
55
|
-
"mkdocs-material~=9.
|
|
64
|
+
"mkdocs-material~=9.6",
|
|
56
65
|
"mkdocs-material-extensions~=1.3",
|
|
57
|
-
"pygments~=2.
|
|
58
|
-
"pymdown-extensions~=10.
|
|
66
|
+
"pygments~=2.19",
|
|
67
|
+
"pymdown-extensions~=10.15",
|
|
59
68
|
]
|
|
60
69
|
|
|
61
|
-
[tool.
|
|
62
|
-
|
|
63
|
-
"py39",
|
|
64
|
-
]
|
|
70
|
+
[tool.phosphorus.dynamic]
|
|
71
|
+
version = { file = "src/pyutilkit/__version__.py" }
|
|
65
72
|
|
|
66
73
|
[tool.mypy]
|
|
67
74
|
check_untyped_defs = true
|
|
@@ -95,7 +102,7 @@ disallow_any_decorated = false # mock.MagicMock is Any
|
|
|
95
102
|
src = [
|
|
96
103
|
"src",
|
|
97
104
|
]
|
|
98
|
-
target-version = "
|
|
105
|
+
target-version = "py310"
|
|
99
106
|
|
|
100
107
|
[tool.ruff.lint]
|
|
101
108
|
select = [
|
|
@@ -103,11 +110,10 @@ select = [
|
|
|
103
110
|
]
|
|
104
111
|
ignore = [
|
|
105
112
|
"C901", # Adding a limit to complexity is too arbitrary
|
|
106
|
-
"COM812", # Avoid
|
|
113
|
+
"COM812", # Avoid conflicts with the formatter
|
|
107
114
|
"D10", # Not everything needs a docstring
|
|
108
115
|
"D203", # Prefer `no-blank-line-before-class` (D211)
|
|
109
116
|
"D213", # Prefer `multi-line-summary-first-line` (D212)
|
|
110
|
-
"E501", # Avoid clashes with black
|
|
111
117
|
"PLR09", # Adding a limit to complexity is too arbitrary
|
|
112
118
|
]
|
|
113
119
|
|
|
@@ -133,8 +139,13 @@ forced-separate = [
|
|
|
133
139
|
split-on-trailing-comma = false
|
|
134
140
|
|
|
135
141
|
[tool.pytest.ini_options]
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
minversion = "9.0"
|
|
143
|
+
strict = true
|
|
144
|
+
addopts = ["-ra", "-v"]
|
|
145
|
+
testpaths = [
|
|
146
|
+
"tests",
|
|
147
|
+
"integration",
|
|
148
|
+
]
|
|
138
149
|
|
|
139
150
|
[tool.coverage.run]
|
|
140
151
|
branch = true
|
|
@@ -142,12 +153,17 @@ source = [
|
|
|
142
153
|
"src/",
|
|
143
154
|
]
|
|
144
155
|
data_file = ".cov_cache/coverage.dat"
|
|
156
|
+
omit = [
|
|
157
|
+
"src/**/type_defs.py",
|
|
158
|
+
]
|
|
145
159
|
|
|
146
160
|
[tool.coverage.report]
|
|
147
161
|
exclude_also = [
|
|
148
162
|
"if TYPE_CHECKING:",
|
|
163
|
+
"raise NotImplementedError",
|
|
164
|
+
"raise UnreachableCodeError",
|
|
149
165
|
]
|
|
150
|
-
fail_under =
|
|
166
|
+
fail_under = 100
|
|
151
167
|
precision = 2
|
|
152
168
|
show_missing = true
|
|
153
169
|
skip_covered = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.11.0"
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import threading
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Singleton(type):
|
|
7
|
-
instance: type
|
|
7
|
+
instance: type | None
|
|
8
8
|
|
|
9
9
|
def __init__(
|
|
10
10
|
cls, name: str, bases: tuple[type[object], ...], namespace: dict[str, object]
|
|
11
11
|
) -> None:
|
|
12
12
|
super().__init__(name, bases, namespace)
|
|
13
13
|
cls.instance = None
|
|
14
|
+
cls._lock = threading.Lock()
|
|
14
15
|
|
|
15
|
-
def __call__(cls) -> type
|
|
16
|
+
def __call__(cls) -> type:
|
|
16
17
|
if cls.instance is None:
|
|
17
|
-
cls.
|
|
18
|
-
|
|
18
|
+
with cls._lock:
|
|
19
|
+
if cls.instance is None:
|
|
20
|
+
cls.instance = super().__call__()
|
|
21
|
+
return cls.instance
|
|
@@ -3,19 +3,16 @@ from __future__ import annotations
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import logging
|
|
5
5
|
from functools import wraps
|
|
6
|
-
from typing import TYPE_CHECKING, TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, ParamSpec, TypeVar
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from collections.abc import Callable
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
from typing_extensions import ParamSpec # upgrade: py3.9: import from typing
|
|
13
|
-
|
|
14
|
-
P = ParamSpec("P")
|
|
15
|
-
|
|
16
12
|
logger = logging.getLogger(__name__)
|
|
17
13
|
INGEST_ERROR = "Function `%s` threw `%s` when called with args=%s and kwargs=%s"
|
|
18
14
|
R_co = TypeVar("R_co", covariant=True)
|
|
15
|
+
P = ParamSpec("P")
|
|
19
16
|
|
|
20
17
|
|
|
21
18
|
def handle_exceptions(
|
|
@@ -67,18 +67,8 @@ class SGRCodes(IntEnum):
|
|
|
67
67
|
return f"\033[{self.value}m"
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
@dataclass(frozen=True, order=True)
|
|
70
|
+
@dataclass(frozen=True, order=True, slots=True)
|
|
71
71
|
class SGRString:
|
|
72
|
-
__slots__ = ( # upgrade: py3.9: use __slots__ = True
|
|
73
|
-
"_force_prefix",
|
|
74
|
-
"_force_sgr",
|
|
75
|
-
"_is_error",
|
|
76
|
-
"_prefix",
|
|
77
|
-
"_sgr",
|
|
78
|
-
"_string",
|
|
79
|
-
"_suffix",
|
|
80
|
-
)
|
|
81
|
-
|
|
82
72
|
_string: str
|
|
83
73
|
_sgr: tuple[SGRCodes, ...]
|
|
84
74
|
_prefix: str
|
|
@@ -215,14 +205,14 @@ class SGRString:
|
|
|
215
205
|
type(self)(self._string, prefix=prefix, suffix=suffix, params=self._sgr).print()
|
|
216
206
|
|
|
217
207
|
|
|
218
|
-
@dataclass(frozen=True, order=True)
|
|
208
|
+
@dataclass(frozen=True, order=True, slots=True)
|
|
219
209
|
class SGROutput:
|
|
220
|
-
__slots__ = ("_strings",) # upgrade: py3.9: use __slots__ = True
|
|
221
210
|
_strings: tuple[SGRString, ...]
|
|
222
211
|
|
|
223
212
|
def __init__(
|
|
224
213
|
self,
|
|
225
214
|
strings: Iterable[object],
|
|
215
|
+
*,
|
|
226
216
|
force_prefix: bool | None = None,
|
|
227
217
|
force_sgr: bool | None = None,
|
|
228
218
|
is_error: bool | None = None,
|
|
@@ -242,6 +232,7 @@ class SGROutput:
|
|
|
242
232
|
@staticmethod
|
|
243
233
|
def _clean_string(
|
|
244
234
|
string: object,
|
|
235
|
+
*,
|
|
245
236
|
force_prefix: bool | None,
|
|
246
237
|
force_sgr: bool | None,
|
|
247
238
|
is_error: bool | None,
|
|
@@ -5,21 +5,20 @@ from time import perf_counter_ns
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterator
|
|
8
9
|
from types import TracebackType
|
|
9
10
|
|
|
10
11
|
from typing_extensions import Self # upgrade: py3.10: import from typing
|
|
11
12
|
|
|
12
|
-
METRIC_MULTIPLIER =
|
|
13
|
+
METRIC_MULTIPLIER = 1_000
|
|
13
14
|
SECONDS_PER_MINUTE = 60
|
|
14
15
|
MINUTES_PER_HOUR = 60
|
|
15
16
|
HOURS_PER_DAY = 24
|
|
16
17
|
SECONDS_PER_DAY = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
@dataclass(frozen=True, order=True)
|
|
20
|
+
@dataclass(frozen=True, order=True, slots=True)
|
|
20
21
|
class Timing:
|
|
21
|
-
__slots__ = ("nanoseconds",) # upgrade: py3.9: use __slots__ = True
|
|
22
|
-
|
|
23
22
|
nanoseconds: int
|
|
24
23
|
|
|
25
24
|
def __init__(
|
|
@@ -41,28 +40,35 @@ class Timing:
|
|
|
41
40
|
object.__setattr__(self, "nanoseconds", total_nanoseconds)
|
|
42
41
|
|
|
43
42
|
def __str__(self) -> str:
|
|
44
|
-
if self.nanoseconds
|
|
45
|
-
return
|
|
46
|
-
|
|
43
|
+
if self.nanoseconds == 0:
|
|
44
|
+
return "0ns"
|
|
45
|
+
sign = "" if self.nanoseconds > 0 else "-"
|
|
46
|
+
nanoseconds = abs(self.nanoseconds)
|
|
47
|
+
if nanoseconds < METRIC_MULTIPLIER:
|
|
48
|
+
return f"{sign}{nanoseconds}ns"
|
|
49
|
+
microseconds = nanoseconds / METRIC_MULTIPLIER
|
|
47
50
|
if microseconds < METRIC_MULTIPLIER:
|
|
48
|
-
return f"{microseconds:.1f}µs"
|
|
51
|
+
return f"{sign}{microseconds:.1f}µs"
|
|
49
52
|
milliseconds = microseconds / METRIC_MULTIPLIER
|
|
50
53
|
if milliseconds < METRIC_MULTIPLIER:
|
|
51
|
-
return f"{milliseconds:.1f}ms"
|
|
54
|
+
return f"{sign}{milliseconds:.1f}ms"
|
|
52
55
|
seconds = milliseconds / METRIC_MULTIPLIER
|
|
53
56
|
if seconds < SECONDS_PER_MINUTE:
|
|
54
|
-
return f"{seconds:.2f}s"
|
|
57
|
+
return f"{sign}{seconds:.2f}s"
|
|
55
58
|
round_seconds = int(seconds)
|
|
56
59
|
minutes, seconds = divmod(round_seconds, SECONDS_PER_MINUTE)
|
|
57
60
|
hours, minutes = divmod(minutes, MINUTES_PER_HOUR)
|
|
58
61
|
if hours < HOURS_PER_DAY:
|
|
59
|
-
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
62
|
+
return f"{sign}{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
60
63
|
days, hours = divmod(hours, HOURS_PER_DAY)
|
|
61
|
-
return f"{days:,}d {hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
64
|
+
return f"{sign}{days:,}d {hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
62
65
|
|
|
63
66
|
def __bool__(self) -> bool:
|
|
64
67
|
return bool(self.nanoseconds)
|
|
65
68
|
|
|
69
|
+
def __neg__(self) -> Self:
|
|
70
|
+
return self.__class__(nanoseconds=-self.nanoseconds)
|
|
71
|
+
|
|
66
72
|
def __add__(self, other: object) -> Timing:
|
|
67
73
|
if not isinstance(other, Timing):
|
|
68
74
|
return NotImplemented
|
|
@@ -73,6 +79,16 @@ class Timing:
|
|
|
73
79
|
return NotImplemented
|
|
74
80
|
return Timing(nanoseconds=self.nanoseconds + other.nanoseconds)
|
|
75
81
|
|
|
82
|
+
def __sub__(self, other: object) -> Timing:
|
|
83
|
+
if not isinstance(other, Timing):
|
|
84
|
+
return NotImplemented
|
|
85
|
+
return Timing(nanoseconds=self.nanoseconds - other.nanoseconds)
|
|
86
|
+
|
|
87
|
+
def __rsub__(self, other: object) -> Timing:
|
|
88
|
+
if not isinstance(other, Timing):
|
|
89
|
+
return NotImplemented
|
|
90
|
+
return Timing(nanoseconds=other.nanoseconds - self.nanoseconds)
|
|
91
|
+
|
|
76
92
|
def __mul__(self, other: object) -> Timing:
|
|
77
93
|
if not isinstance(other, int):
|
|
78
94
|
return NotImplemented
|
|
@@ -97,10 +113,11 @@ class Timing:
|
|
|
97
113
|
class Stopwatch:
|
|
98
114
|
_start: int
|
|
99
115
|
laps: list[Timing]
|
|
116
|
+
__slots__ = ("_start", "_zero", "laps")
|
|
100
117
|
|
|
101
118
|
def __init__(self) -> None:
|
|
102
|
-
self._start = 0
|
|
103
119
|
self.laps: list[Timing] = []
|
|
120
|
+
self._zero = Timing()
|
|
104
121
|
|
|
105
122
|
def __enter__(self) -> Self:
|
|
106
123
|
self._start = perf_counter_ns()
|
|
@@ -112,19 +129,37 @@ class Stopwatch:
|
|
|
112
129
|
exc_value: BaseException | None,
|
|
113
130
|
traceback: TracebackType | None,
|
|
114
131
|
) -> None:
|
|
115
|
-
|
|
116
|
-
self.laps.append(Timing(nanoseconds=
|
|
132
|
+
end = perf_counter_ns()
|
|
133
|
+
self.laps.append(Timing(nanoseconds=end - self._start))
|
|
134
|
+
del self._start
|
|
117
135
|
|
|
118
136
|
def __bool__(self) -> bool:
|
|
119
137
|
return bool(self.elapsed)
|
|
120
138
|
|
|
139
|
+
def __len__(self) -> int:
|
|
140
|
+
return len(self.laps)
|
|
141
|
+
|
|
142
|
+
def __iter__(self) -> Iterator[Timing]:
|
|
143
|
+
return iter(self.laps)
|
|
144
|
+
|
|
145
|
+
def reset(self) -> None:
|
|
146
|
+
self.laps.clear()
|
|
147
|
+
|
|
121
148
|
@property
|
|
122
149
|
def elapsed(self) -> Timing:
|
|
123
|
-
return sum(self.laps,
|
|
150
|
+
return sum(self.laps, self._zero)
|
|
124
151
|
|
|
125
152
|
@property
|
|
126
153
|
def average(self) -> Timing:
|
|
127
154
|
if not self.laps:
|
|
128
155
|
msg = "No laps recorded"
|
|
129
156
|
raise ZeroDivisionError(msg)
|
|
130
|
-
return self.elapsed // len(self
|
|
157
|
+
return self.elapsed // len(self)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def min(self) -> Timing:
|
|
161
|
+
return min(self.laps)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def max(self) -> Timing:
|
|
165
|
+
return max(self.laps)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|