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 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
+ [![CI](https://github.com/NakuRei/ticko/actions/workflows/ci.yml/badge.svg)](https://github.com/NakuRei/ticko/actions/workflows/ci.yml)
28
+ [![codecov](https://codecov.io/gh/NakuRei/ticko/branch/main/graph/badge.svg)](https://codecov.io/gh/NakuRei/ticko)
29
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![CI](https://github.com/NakuRei/ticko/actions/workflows/ci.yml/badge.svg)](https://github.com/NakuRei/ticko/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/NakuRei/ticko/branch/main/graph/badge.svg)](https://codecov.io/gh/NakuRei/ticko)
5
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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()