ticko 1.0.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.
- ticko-1.0.0/PKG-INFO +172 -0
- ticko-1.0.0/README.md +148 -0
- ticko-1.0.0/pyproject.toml +71 -0
- ticko-1.0.0/src/ticko/__init__.py +56 -0
- ticko-1.0.0/src/ticko/decorators.py +109 -0
- ticko-1.0.0/src/ticko/py.typed +0 -0
- ticko-1.0.0/src/ticko/stop_watch.py +323 -0
ticko-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ticko
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Thread-safe stopwatch for measuring elapsed time
|
|
5
|
+
Keywords: stopwatch,timer,performance,profiling,benchmark
|
|
6
|
+
Author: NakuRei
|
|
7
|
+
Author-email: NakuRei <nakurei7901@gmail.com>
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: System :: Benchmark
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Project-URL: Homepage, https://github.com/NakuRei/ticko
|
|
21
|
+
Project-URL: Issues, https://github.com/NakuRei/ticko/issues
|
|
22
|
+
Project-URL: Repository, https://github.com/NakuRei/ticko
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# ticko
|
|
26
|
+
|
|
27
|
+
[](https://github.com/NakuRei/ticko/actions/workflows/ci.yml)
|
|
28
|
+
[](https://codecov.io/gh/NakuRei/ticko)
|
|
29
|
+
[](https://www.python.org/downloads/)
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
|
|
32
|
+
A modern, thread-safe stopwatch library for Python.
|
|
33
|
+
|
|
34
|
+
## Why ticko?
|
|
35
|
+
|
|
36
|
+
- **Thread-safe by design** - Use confidently in concurrent applications
|
|
37
|
+
- **Type-safe** - Full type hints for excellent IDE support
|
|
38
|
+
- **Zero dependencies** - Pure Python, no external requirements
|
|
39
|
+
- **Flexible API** - Context managers, decorators, or manual control
|
|
40
|
+
- **Production-ready** - Comprehensive test coverage
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install ticko
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from ticko import StopWatch
|
|
52
|
+
|
|
53
|
+
# Basic usage
|
|
54
|
+
with StopWatch() as sw:
|
|
55
|
+
# Your code here
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
print(f"Elapsed: {sw.time_elapsed:.2f}s")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from ticko import stopwatch
|
|
63
|
+
|
|
64
|
+
# Decorator for function timing
|
|
65
|
+
@stopwatch
|
|
66
|
+
def process_data():
|
|
67
|
+
# Your code here
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
process_data() # Automatically prints execution time
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Core Features
|
|
74
|
+
|
|
75
|
+
### Manual Control
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
sw = StopWatch()
|
|
79
|
+
sw.start()
|
|
80
|
+
# ... your code ...
|
|
81
|
+
elapsed = sw.stop()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Lap Timing
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
sw = StopWatch()
|
|
88
|
+
sw.start()
|
|
89
|
+
|
|
90
|
+
# Record multiple laps
|
|
91
|
+
lap1 = sw.lap()
|
|
92
|
+
lap2 = sw.lap()
|
|
93
|
+
|
|
94
|
+
elapsed =sw.stop()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Custom Callbacks
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
def log_time(sw: StopWatch):
|
|
101
|
+
logger.info(f"Execution took {sw.time_elapsed:.3f}s")
|
|
102
|
+
|
|
103
|
+
@stopwatch(exit_callback=log_time)
|
|
104
|
+
def my_function():
|
|
105
|
+
pass
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Thread Safety
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
112
|
+
|
|
113
|
+
sw = StopWatch()
|
|
114
|
+
sw.start()
|
|
115
|
+
|
|
116
|
+
# Multiple threads can safely share one StopWatch
|
|
117
|
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
|
118
|
+
futures = [executor.submit(sw.lap) for _ in range(10)]
|
|
119
|
+
|
|
120
|
+
elapsed =sw.stop()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For more examples, see the [`examples/`](examples/) directory.
|
|
124
|
+
|
|
125
|
+
## API Overview
|
|
126
|
+
|
|
127
|
+
### `StopWatch`
|
|
128
|
+
|
|
129
|
+
**Properties:**
|
|
130
|
+
- `is_running: bool` - Current state
|
|
131
|
+
- `time_elapsed: float` - Total elapsed time
|
|
132
|
+
- `time_last_lap: float` - Last lap duration
|
|
133
|
+
|
|
134
|
+
**Methods:**
|
|
135
|
+
- `start()` - Start timing
|
|
136
|
+
- `stop()` - Stop and return elapsed time
|
|
137
|
+
- `lap()` - Record lap time
|
|
138
|
+
- `reset()` - Reset to initial state
|
|
139
|
+
|
|
140
|
+
### `@stopwatch`
|
|
141
|
+
|
|
142
|
+
Decorator for automatic function timing with optional custom callbacks.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Install with dev dependencies
|
|
148
|
+
uv sync --dev
|
|
149
|
+
|
|
150
|
+
# Run tests
|
|
151
|
+
pytest tests/
|
|
152
|
+
|
|
153
|
+
# Run tests with coverage report
|
|
154
|
+
pytest tests -v --cov=src --cov-report=term-missing --cov-report=xml:cov.xml
|
|
155
|
+
|
|
156
|
+
# Type checking
|
|
157
|
+
mypy .
|
|
158
|
+
|
|
159
|
+
# Lint checking
|
|
160
|
+
ruff check
|
|
161
|
+
|
|
162
|
+
# Format checking
|
|
163
|
+
ruff format --check --diff
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT License - Copyright (c) 2025 NakuRei
|
|
169
|
+
|
|
170
|
+
## Contributing
|
|
171
|
+
|
|
172
|
+
Contributions welcome! Feel free to open issues or submit pull requests.
|
ticko-1.0.0/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# ticko
|
|
2
|
+
|
|
3
|
+
[](https://github.com/NakuRei/ticko/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/NakuRei/ticko)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
A modern, thread-safe stopwatch library for Python.
|
|
9
|
+
|
|
10
|
+
## Why ticko?
|
|
11
|
+
|
|
12
|
+
- **Thread-safe by design** - Use confidently in concurrent applications
|
|
13
|
+
- **Type-safe** - Full type hints for excellent IDE support
|
|
14
|
+
- **Zero dependencies** - Pure Python, no external requirements
|
|
15
|
+
- **Flexible API** - Context managers, decorators, or manual control
|
|
16
|
+
- **Production-ready** - Comprehensive test coverage
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install ticko
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from ticko import StopWatch
|
|
28
|
+
|
|
29
|
+
# Basic usage
|
|
30
|
+
with StopWatch() as sw:
|
|
31
|
+
# Your code here
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
print(f"Elapsed: {sw.time_elapsed:.2f}s")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from ticko import stopwatch
|
|
39
|
+
|
|
40
|
+
# Decorator for function timing
|
|
41
|
+
@stopwatch
|
|
42
|
+
def process_data():
|
|
43
|
+
# Your code here
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
process_data() # Automatically prints execution time
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Core Features
|
|
50
|
+
|
|
51
|
+
### Manual Control
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
sw = StopWatch()
|
|
55
|
+
sw.start()
|
|
56
|
+
# ... your code ...
|
|
57
|
+
elapsed = sw.stop()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Lap Timing
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
sw = StopWatch()
|
|
64
|
+
sw.start()
|
|
65
|
+
|
|
66
|
+
# Record multiple laps
|
|
67
|
+
lap1 = sw.lap()
|
|
68
|
+
lap2 = sw.lap()
|
|
69
|
+
|
|
70
|
+
elapsed =sw.stop()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Custom Callbacks
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
def log_time(sw: StopWatch):
|
|
77
|
+
logger.info(f"Execution took {sw.time_elapsed:.3f}s")
|
|
78
|
+
|
|
79
|
+
@stopwatch(exit_callback=log_time)
|
|
80
|
+
def my_function():
|
|
81
|
+
pass
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Thread Safety
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
88
|
+
|
|
89
|
+
sw = StopWatch()
|
|
90
|
+
sw.start()
|
|
91
|
+
|
|
92
|
+
# Multiple threads can safely share one StopWatch
|
|
93
|
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
|
94
|
+
futures = [executor.submit(sw.lap) for _ in range(10)]
|
|
95
|
+
|
|
96
|
+
elapsed =sw.stop()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
For more examples, see the [`examples/`](examples/) directory.
|
|
100
|
+
|
|
101
|
+
## API Overview
|
|
102
|
+
|
|
103
|
+
### `StopWatch`
|
|
104
|
+
|
|
105
|
+
**Properties:**
|
|
106
|
+
- `is_running: bool` - Current state
|
|
107
|
+
- `time_elapsed: float` - Total elapsed time
|
|
108
|
+
- `time_last_lap: float` - Last lap duration
|
|
109
|
+
|
|
110
|
+
**Methods:**
|
|
111
|
+
- `start()` - Start timing
|
|
112
|
+
- `stop()` - Stop and return elapsed time
|
|
113
|
+
- `lap()` - Record lap time
|
|
114
|
+
- `reset()` - Reset to initial state
|
|
115
|
+
|
|
116
|
+
### `@stopwatch`
|
|
117
|
+
|
|
118
|
+
Decorator for automatic function timing with optional custom callbacks.
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Install with dev dependencies
|
|
124
|
+
uv sync --dev
|
|
125
|
+
|
|
126
|
+
# Run tests
|
|
127
|
+
pytest tests/
|
|
128
|
+
|
|
129
|
+
# Run tests with coverage report
|
|
130
|
+
pytest tests -v --cov=src --cov-report=term-missing --cov-report=xml:cov.xml
|
|
131
|
+
|
|
132
|
+
# Type checking
|
|
133
|
+
mypy .
|
|
134
|
+
|
|
135
|
+
# Lint checking
|
|
136
|
+
ruff check
|
|
137
|
+
|
|
138
|
+
# Format checking
|
|
139
|
+
ruff format --check --diff
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT License - Copyright (c) 2025 NakuRei
|
|
145
|
+
|
|
146
|
+
## Contributing
|
|
147
|
+
|
|
148
|
+
Contributions welcome! Feel free to open issues or submit pull requests.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ticko"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Thread-safe stopwatch for measuring elapsed time"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "NakuRei", email = "nakurei7901@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
keywords = ["stopwatch", "timer", "performance", "profiling", "benchmark"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Topic :: System :: Benchmark",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/NakuRei/ticko"
|
|
28
|
+
Repository = "https://github.com/NakuRei/ticko"
|
|
29
|
+
Issues = "https://github.com/NakuRei/ticko/issues"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.9.4,<0.10.0"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"lxml>=6.0.2",
|
|
38
|
+
"mypy>=1.18.2",
|
|
39
|
+
"pytest>=8.4.2",
|
|
40
|
+
"pytest-cov>=7.0.0",
|
|
41
|
+
"ruff>=0.14.1",
|
|
42
|
+
"ticko",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 80
|
|
47
|
+
indent-width = 4
|
|
48
|
+
target-version = "py310"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = ["ALL"]
|
|
52
|
+
ignore = [
|
|
53
|
+
"S101", # assert
|
|
54
|
+
"D203", # incorrect-blank-line-before-class
|
|
55
|
+
"D213", # multi-line-summary-second-line
|
|
56
|
+
"COM812", # disabled to prevent conflicts with the formatter (trailing comma handling)
|
|
57
|
+
]
|
|
58
|
+
per-file-ignores = { "examples/**" = ["T201", "INP001"], "tests/**" = ["INP001", "PLR2004", "ARG005", "D401", "D404", "TRY003", "EM101", "BLE001", "PERF203"] }
|
|
59
|
+
|
|
60
|
+
[tool.ruff.format]
|
|
61
|
+
quote-style = "double"
|
|
62
|
+
indent-style = "space"
|
|
63
|
+
line-ending = "lf"
|
|
64
|
+
|
|
65
|
+
[tool.uv.workspace]
|
|
66
|
+
members = [
|
|
67
|
+
".",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.uv.sources]
|
|
71
|
+
ticko = { workspace = true, editable = true }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Ticko: A simple and flexible stopwatch library for Python.
|
|
2
|
+
|
|
3
|
+
This package provides utilities for measuring execution time in Python programs.
|
|
4
|
+
It includes a thread-safe StopWatch class for manual timing control and a
|
|
5
|
+
decorator for automatically measuring function execution times.
|
|
6
|
+
|
|
7
|
+
Classes
|
|
8
|
+
-------
|
|
9
|
+
StopWatch
|
|
10
|
+
Thread-safe stopwatch for measuring elapsed time with start, stop, lap,
|
|
11
|
+
and reset functionality.
|
|
12
|
+
|
|
13
|
+
Functions
|
|
14
|
+
---------
|
|
15
|
+
stopwatch
|
|
16
|
+
Decorator that measures and reports the execution time of a function.
|
|
17
|
+
|
|
18
|
+
Examples
|
|
19
|
+
--------
|
|
20
|
+
Using the decorator:
|
|
21
|
+
|
|
22
|
+
>>> @stopwatch
|
|
23
|
+
... def compute(n):
|
|
24
|
+
... return sum(range(n))
|
|
25
|
+
>>> compute(1000)
|
|
26
|
+
Function 'compute' executed in 0.000123 seconds
|
|
27
|
+
499500
|
|
28
|
+
|
|
29
|
+
Using the StopWatch class directly:
|
|
30
|
+
|
|
31
|
+
>>> sw = StopWatch()
|
|
32
|
+
>>> sw.start()
|
|
33
|
+
>>> # ... do some work ...
|
|
34
|
+
>>> sw.lap()
|
|
35
|
+
1.234
|
|
36
|
+
>>> # ... do more work ...
|
|
37
|
+
>>> sw.stop()
|
|
38
|
+
2.567
|
|
39
|
+
|
|
40
|
+
Using StopWatch as a context manager:
|
|
41
|
+
|
|
42
|
+
>>> with StopWatch() as sw:
|
|
43
|
+
... # ... do some work ...
|
|
44
|
+
... pass
|
|
45
|
+
>>> sw.time_elapsed
|
|
46
|
+
1.234
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from .decorators import stopwatch
|
|
51
|
+
from .stop_watch import StopWatch
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"StopWatch",
|
|
55
|
+
"stopwatch",
|
|
56
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Decorator for measuring function execution time."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import ParamSpec, TypeVar, overload
|
|
8
|
+
|
|
9
|
+
from .stop_watch import StopWatch
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
P = ParamSpec("P")
|
|
14
|
+
R = TypeVar("R")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Overload 1: Decorator without arguments (@stopwatch)
|
|
18
|
+
@overload
|
|
19
|
+
def stopwatch(
|
|
20
|
+
func: Callable[P, R],
|
|
21
|
+
) -> Callable[P, R]: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Overload 2: Decorator with arguments (@stopwatch(...))
|
|
25
|
+
@overload
|
|
26
|
+
def stopwatch(
|
|
27
|
+
func: None = None,
|
|
28
|
+
*,
|
|
29
|
+
timer_func: Callable[[], float] = time.perf_counter,
|
|
30
|
+
exit_callback: Callable[[StopWatch], None] | None = None,
|
|
31
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Implementation
|
|
35
|
+
def stopwatch(
|
|
36
|
+
func: Callable[P, R] | None = None,
|
|
37
|
+
*,
|
|
38
|
+
timer_func: Callable[[], float] = time.perf_counter,
|
|
39
|
+
exit_callback: Callable[[StopWatch], None] | None = None,
|
|
40
|
+
) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]:
|
|
41
|
+
"""Measure the execution time of a function using StopWatch.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
func : Callable[P, R] | None, optional
|
|
46
|
+
The function to decorate. If None, returns a decorator function.
|
|
47
|
+
timer_func : Callable[[], float], optional
|
|
48
|
+
Function returning the current time (default: time.perf_counter).
|
|
49
|
+
exit_callback : Callable[[StopWatch], None] | None, optional
|
|
50
|
+
Optional callback invoked with the StopWatch instance when the
|
|
51
|
+
decorated function exits. If None, a default callback is used that
|
|
52
|
+
prints the elapsed time to standard output.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
Callable[P, R] or Callable[[Callable[P, R]], Callable[P, R]]
|
|
57
|
+
The decorated function, or a decorator function when func is None.
|
|
58
|
+
|
|
59
|
+
Notes
|
|
60
|
+
-----
|
|
61
|
+
The StopWatch is stopped regardless of whether the decorated function
|
|
62
|
+
returns normally or raises an exception. If an exception occurs it is
|
|
63
|
+
re-raised after the stopwatch has been stopped; exit_callback is still
|
|
64
|
+
invoked.
|
|
65
|
+
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
>>> @stopwatch
|
|
69
|
+
... def f(x):
|
|
70
|
+
... return x * 2
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def _create_wrapper(f: Callable[P, R]) -> Callable[P, R]:
|
|
75
|
+
"""Create the wrapper function for the given function."""
|
|
76
|
+
|
|
77
|
+
@functools.wraps(f)
|
|
78
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
79
|
+
callback = exit_callback
|
|
80
|
+
if callback is None:
|
|
81
|
+
|
|
82
|
+
def _default_callback(sw: StopWatch) -> None:
|
|
83
|
+
print( # noqa: T201
|
|
84
|
+
f"Function {f.__name__!r} executed "
|
|
85
|
+
f"in {sw.time_elapsed:.6f} seconds",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
callback = _default_callback
|
|
89
|
+
|
|
90
|
+
sw = StopWatch(timer_func=timer_func, exit_callback=callback)
|
|
91
|
+
sw.start()
|
|
92
|
+
try:
|
|
93
|
+
return f(*args, **kwargs)
|
|
94
|
+
except Exception:
|
|
95
|
+
logger.exception(
|
|
96
|
+
"Exception in stopwatch-decorated function",
|
|
97
|
+
)
|
|
98
|
+
raise
|
|
99
|
+
finally:
|
|
100
|
+
sw.stop()
|
|
101
|
+
|
|
102
|
+
return wrapper
|
|
103
|
+
|
|
104
|
+
if func is None:
|
|
105
|
+
# Return a decorator function
|
|
106
|
+
return _create_wrapper
|
|
107
|
+
|
|
108
|
+
# Apply decorator directly
|
|
109
|
+
return _create_wrapper(func)
|
|
File without changes
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Thread-safe stopwatch for measuring elapsed time."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Final, final
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StopWatchError(Exception):
|
|
14
|
+
"""Base class for StopWatch exceptions."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InvalidStateError(StopWatchError):
|
|
18
|
+
"""Raised when an operation is attempted in an invalid state."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AlreadyRunningError(StopWatchError):
|
|
22
|
+
"""Raised when trying to start an already running stopwatch."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotStartedError(StopWatchError):
|
|
26
|
+
"""Raised when stopping or lapping a stopwatch that hasn't been started."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@final
|
|
30
|
+
class StopWatch:
|
|
31
|
+
"""Thread-safe stopwatch for measuring elapsed time.
|
|
32
|
+
|
|
33
|
+
This class provides methods to start, stop, lap, and reset a stopwatch. It
|
|
34
|
+
is designed to be thread-safe, allowing safe usage in multi-threaded
|
|
35
|
+
environments.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
timer_func : Callable[[], float], optional
|
|
40
|
+
Function returning the current time (default: time.perf_counter).
|
|
41
|
+
exit_callback : Callable[[StopWatch], None] | None, optional
|
|
42
|
+
Optional callback invoked with the StopWatch instance when the
|
|
43
|
+
stopwatch is stopped. If None, no callback is invoked.
|
|
44
|
+
|
|
45
|
+
Attributes
|
|
46
|
+
----------
|
|
47
|
+
is_running : bool
|
|
48
|
+
Indicates whether the stopwatch is currently running.
|
|
49
|
+
time_start : float | None
|
|
50
|
+
The start time of the stopwatch, or None if not started.
|
|
51
|
+
time_stop : float | None
|
|
52
|
+
The stop time of the stopwatch, or None if not stopped.
|
|
53
|
+
time_last_lap_start : float | None
|
|
54
|
+
The start time of the last lap, or None if no laps recorded.
|
|
55
|
+
time_elapsed : float
|
|
56
|
+
The total elapsed time since the stopwatch was started.
|
|
57
|
+
time_last_lap : float
|
|
58
|
+
The elapsed time of the last lap.
|
|
59
|
+
|
|
60
|
+
Methods
|
|
61
|
+
-------
|
|
62
|
+
start() -> float
|
|
63
|
+
Start the stopwatch.
|
|
64
|
+
lap() -> float
|
|
65
|
+
Record a lap time.
|
|
66
|
+
stop() -> float
|
|
67
|
+
Stop the stopwatch.
|
|
68
|
+
reset() -> None
|
|
69
|
+
Reset the stopwatch to its initial state.
|
|
70
|
+
|
|
71
|
+
Examples
|
|
72
|
+
--------
|
|
73
|
+
>>> sw = StopWatch()
|
|
74
|
+
>>> sw.start()
|
|
75
|
+
>>> time.sleep(1)
|
|
76
|
+
>>> sw.lap()
|
|
77
|
+
1.0
|
|
78
|
+
>>> time.sleep(2)
|
|
79
|
+
>>> sw.stop()
|
|
80
|
+
3.0
|
|
81
|
+
>>> sw.time_elapsed
|
|
82
|
+
3.0
|
|
83
|
+
>>> sw.reset()
|
|
84
|
+
>>> sw.time_elapsed
|
|
85
|
+
Traceback (most recent call last):
|
|
86
|
+
...
|
|
87
|
+
NotStartedError: ...
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
timer_func: Callable[[], float] = time.perf_counter,
|
|
94
|
+
exit_callback: Callable[["StopWatch"], None] | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Initialize the stopwatch.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
timer_func : Callable[[], float], optional
|
|
101
|
+
Function returning the current time (default: time.perf_counter).
|
|
102
|
+
exit_callback : Callable[[StopWatch], None] | None, optional
|
|
103
|
+
Optional callback invoked when the stopwatch is stopped.
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
self._timer_func: Final = timer_func
|
|
107
|
+
self._exit_callback: Final = exit_callback
|
|
108
|
+
|
|
109
|
+
self._time_start: float | None = None
|
|
110
|
+
self._time_last_lap_start: float | None = None
|
|
111
|
+
self._time_stop: float | None = None
|
|
112
|
+
self._is_running: bool = False
|
|
113
|
+
|
|
114
|
+
self._lock = threading.Lock() # For thread safety
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def is_running(self) -> bool:
|
|
118
|
+
"""Check if the stopwatch is currently running."""
|
|
119
|
+
with self._lock:
|
|
120
|
+
return self._is_running
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def time_start(self) -> float | None:
|
|
124
|
+
"""Get the start time of the stopwatch."""
|
|
125
|
+
with self._lock:
|
|
126
|
+
return self._time_start
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def time_stop(self) -> float | None:
|
|
130
|
+
"""Get the stop time of the stopwatch."""
|
|
131
|
+
with self._lock:
|
|
132
|
+
return self._time_stop
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def time_last_lap_start(self) -> float | None:
|
|
136
|
+
"""Get the start time of the last lap."""
|
|
137
|
+
with self._lock:
|
|
138
|
+
return self._time_last_lap_start
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def time_elapsed(self) -> float:
|
|
142
|
+
"""Get the total elapsed time."""
|
|
143
|
+
with self._lock:
|
|
144
|
+
if self._time_start is None:
|
|
145
|
+
msg = (
|
|
146
|
+
"Stopwatch has not been started. "
|
|
147
|
+
"Call start() before getting elapsed time."
|
|
148
|
+
)
|
|
149
|
+
raise NotStartedError(msg)
|
|
150
|
+
|
|
151
|
+
if self._is_running:
|
|
152
|
+
return self._timer_func() - self._time_start
|
|
153
|
+
if self._time_stop is not None:
|
|
154
|
+
return self._time_stop - self._time_start
|
|
155
|
+
# This is unreachable
|
|
156
|
+
msg = (
|
|
157
|
+
"Stopwatch is in an invalid state. "
|
|
158
|
+
"Call reset() to reinitialize."
|
|
159
|
+
)
|
|
160
|
+
raise InvalidStateError(msg)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def time_last_lap(self) -> float:
|
|
164
|
+
"""Get the elapsed time of the last lap."""
|
|
165
|
+
with self._lock:
|
|
166
|
+
if self._time_last_lap_start is None:
|
|
167
|
+
msg = (
|
|
168
|
+
"No laps have been recorded. "
|
|
169
|
+
"Call lap() after starting the stopwatch."
|
|
170
|
+
)
|
|
171
|
+
raise NotStartedError(msg)
|
|
172
|
+
|
|
173
|
+
if self._is_running:
|
|
174
|
+
return self._timer_func() - self._time_last_lap_start
|
|
175
|
+
if self._time_stop is not None:
|
|
176
|
+
return self._time_stop - self._time_last_lap_start
|
|
177
|
+
# This is unreachable
|
|
178
|
+
msg = (
|
|
179
|
+
"Stopwatch is in an invalid state. "
|
|
180
|
+
"Call reset() to reinitialize."
|
|
181
|
+
)
|
|
182
|
+
raise InvalidStateError(msg)
|
|
183
|
+
|
|
184
|
+
def reset(self) -> None:
|
|
185
|
+
"""Reset the stopwatch to its initial state."""
|
|
186
|
+
with self._lock:
|
|
187
|
+
self._time_start = None
|
|
188
|
+
self._time_last_lap_start = None
|
|
189
|
+
self._time_stop = None
|
|
190
|
+
self._is_running = False
|
|
191
|
+
logger.debug("Stopwatch has been reset.")
|
|
192
|
+
|
|
193
|
+
def start(self) -> float:
|
|
194
|
+
"""Start the stopwatch."""
|
|
195
|
+
with self._lock:
|
|
196
|
+
if self._is_running:
|
|
197
|
+
msg = (
|
|
198
|
+
"Stopwatch is already running. "
|
|
199
|
+
"Stop or reset it before starting again."
|
|
200
|
+
)
|
|
201
|
+
raise AlreadyRunningError(msg)
|
|
202
|
+
time_current: Final = self._timer_func()
|
|
203
|
+
self._time_start = time_current
|
|
204
|
+
self._time_last_lap_start = time_current
|
|
205
|
+
self._time_stop = None
|
|
206
|
+
self._is_running = True
|
|
207
|
+
logger.debug("Stopwatch started at %f.", time_current)
|
|
208
|
+
return time_current
|
|
209
|
+
|
|
210
|
+
def lap(self) -> float:
|
|
211
|
+
"""Record a lap time."""
|
|
212
|
+
with self._lock:
|
|
213
|
+
if not self._is_running:
|
|
214
|
+
msg = (
|
|
215
|
+
"Stopwatch is not running. "
|
|
216
|
+
"Call start() first before recording a lap."
|
|
217
|
+
)
|
|
218
|
+
raise NotStartedError(msg)
|
|
219
|
+
if self._time_last_lap_start is None:
|
|
220
|
+
# Invariant check: should not happen if running
|
|
221
|
+
msg = "Last lap start time should not be None if running."
|
|
222
|
+
raise InvalidStateError(msg)
|
|
223
|
+
|
|
224
|
+
time_current: Final = self._timer_func()
|
|
225
|
+
lap_duration: Final = time_current - self._time_last_lap_start
|
|
226
|
+
self._time_last_lap_start = time_current
|
|
227
|
+
logger.debug(
|
|
228
|
+
"Lap recorded at %f with duration %f.",
|
|
229
|
+
time_current,
|
|
230
|
+
lap_duration,
|
|
231
|
+
)
|
|
232
|
+
return lap_duration
|
|
233
|
+
|
|
234
|
+
def stop(self) -> float:
|
|
235
|
+
"""Stop the stopwatch."""
|
|
236
|
+
with self._lock:
|
|
237
|
+
if not self._is_running:
|
|
238
|
+
msg = (
|
|
239
|
+
"Stopwatch is not running. "
|
|
240
|
+
"Call start() first before stopping."
|
|
241
|
+
)
|
|
242
|
+
raise NotStartedError(msg)
|
|
243
|
+
if self._time_start is None:
|
|
244
|
+
# Invariant check: should not happen if running
|
|
245
|
+
msg = "Start time should not be None if running."
|
|
246
|
+
raise InvalidStateError(msg)
|
|
247
|
+
|
|
248
|
+
time_current: Final = self._timer_func()
|
|
249
|
+
self._time_stop = time_current
|
|
250
|
+
# Directly compute to avoid multiple calls of with self._lock
|
|
251
|
+
time_elapsed: Final = self._time_stop - self._time_start
|
|
252
|
+
self._is_running = False
|
|
253
|
+
logger.debug(
|
|
254
|
+
"Stopwatch stopped at %f with elapsed time %f.",
|
|
255
|
+
time_current,
|
|
256
|
+
time_elapsed,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Call exit_callback outside the lock to avoid deadlock
|
|
260
|
+
# if callback tries to access stopwatch properties
|
|
261
|
+
if self._exit_callback is not None:
|
|
262
|
+
try:
|
|
263
|
+
self._exit_callback(self)
|
|
264
|
+
except Exception:
|
|
265
|
+
logger.exception("Exit callback raised an exception")
|
|
266
|
+
|
|
267
|
+
return time_elapsed
|
|
268
|
+
|
|
269
|
+
def __repr__(self) -> str:
|
|
270
|
+
"""Return a string representation for recreating the StopWatch.
|
|
271
|
+
|
|
272
|
+
Returns a string showing the constructor parameters, following the
|
|
273
|
+
Python convention that repr() should return a string that could be
|
|
274
|
+
used to recreate the object.
|
|
275
|
+
"""
|
|
276
|
+
timer_name = getattr(
|
|
277
|
+
self._timer_func,
|
|
278
|
+
"__name__",
|
|
279
|
+
repr(self._timer_func),
|
|
280
|
+
)
|
|
281
|
+
callback_name = (
|
|
282
|
+
None
|
|
283
|
+
if self._exit_callback is None
|
|
284
|
+
else getattr(
|
|
285
|
+
self._exit_callback,
|
|
286
|
+
"__name__",
|
|
287
|
+
repr(self._exit_callback),
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
return (
|
|
291
|
+
f"StopWatch(timer_func={timer_name}, exit_callback={callback_name})"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def __str__(self) -> str:
|
|
295
|
+
"""Return a human-readable string representation.
|
|
296
|
+
|
|
297
|
+
Returns a string describing the current state of the stopwatch,
|
|
298
|
+
including whether it's running and the elapsed time if applicable.
|
|
299
|
+
"""
|
|
300
|
+
with self._lock:
|
|
301
|
+
if self._time_start is None:
|
|
302
|
+
return "StopWatch(not started)"
|
|
303
|
+
if self._is_running:
|
|
304
|
+
elapsed = self._timer_func() - self._time_start
|
|
305
|
+
return f"StopWatch(running, elapsed={elapsed:.6f}s)"
|
|
306
|
+
if self._time_stop is not None:
|
|
307
|
+
elapsed = self._time_stop - self._time_start
|
|
308
|
+
return f"StopWatch(stopped, elapsed={elapsed:.6f}s)"
|
|
309
|
+
return "StopWatch(invalid state)" # This is unreachable
|
|
310
|
+
|
|
311
|
+
def __enter__(self) -> "StopWatch":
|
|
312
|
+
"""Enter the context manager and start the stopwatch."""
|
|
313
|
+
self.start()
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
def __exit__(
|
|
317
|
+
self,
|
|
318
|
+
exc_type: type[BaseException] | None,
|
|
319
|
+
exc_value: BaseException | None,
|
|
320
|
+
traceback: TracebackType | None,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Exit the context manager and stop the stopwatch."""
|
|
323
|
+
self.stop()
|