Coverage for pyTooling/Stopwatch/__init__.py: 98%

268 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-25 12:33 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|| |_ ___ _ ____ ____ _| |_ ___| |__ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | \___ \| __/ _ \| '_ \ \ /\ / / _` | __/ __| '_ \ # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_ ___) | || (_) | |_) \ V V / (_| | || (__| | | | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \__\___/| .__/ \_/\_/ \__,_|\__\___|_| |_| # 

7# |_| |___/ |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32A stopwatch to measure execution times. 

33 

34.. hint:: 

35 

36 See :ref:`high-level help <COMMON/Stopwatch>` for explanations and usage examples. 

37 

38.. seealso:: 

39 

40 :mod:`pyTooling.Tracing` 

41 |rarr| Nested timespans instead of a single measurement, for tracing an execution. 

42 :mod:`pyTooling.Process` 

43 |rarr| The process' memory usage, next to its runtime. 

44""" 

45from __future__ import annotations 

46 

47from datetime import datetime, timedelta 

48from time import perf_counter_ns 

49from types import TracebackType 

50from typing import Optional as Nullable, Iterator, Self 

51 

52from pyTooling.Common import getFullyQualifiedName 

53from pyTooling.Decorators import export, readonly 

54from pyTooling.MetaClasses import SlottedObject 

55from pyTooling.Exceptions import ToolingException 

56 

57 

58@export 

59class StopwatchError(ToolingException): 

60 """This exception is caused by wrong usage of the stopwatch.""" 

61 

62 

63@export 

64class ExcludeContextManager: 

65 """ 

66 A stopwatch context manager for excluding certain time spans from measurement. 

67 

68 While a normal stopwatch's embedded context manager (re)starts the stopwatch on every *enter* event and pauses the 

69 stopwatch on every *exit* event, this context manager pauses on *enter* events and restarts on every *exit* event. 

70 """ 

71 _stopwatch: Stopwatch #: Reference to the stopwatch. 

72 

73 def __init__(self, stopwatch: Stopwatch) -> None: 

74 """ 

75 Initializes an excluding context manager. 

76 

77 :param stopwatch: Reference to the stopwatch. 

78 """ 

79 self._stopwatch = stopwatch 

80 

81 def __enter__(self) -> Self: 

82 """ 

83 Enter the context and pause the stopwatch. 

84 

85 :returns: Excluding stopwatch context manager instance. 

86 """ 

87 self._stopwatch.Pause() 

88 

89 return self 

90 

91 def __exit__( 

92 self, 

93 exc_type: Nullable[type[BaseException]] = None, 

94 exc_val: Nullable[BaseException] = None, 

95 exc_tb: Nullable[TracebackType] = None 

96 ) -> Nullable[bool]: 

97 """ 

98 Exit the context and restart stopwatch. 

99 

100 :param exc_type: Optional, exception type. 

101 :param exc_val: Optional, exception instance. 

102 :param exc_tb: Optional, exception's traceback. 

103 :returns: ``None`` 

104 """ 

105 self._stopwatch.Resume() 

106 

107 

108@export 

109class Stopwatch(SlottedObject): 

110 """ 

111 The stopwatch implements a solution to measure and collect timings. 

112 

113 The time measurement can be started, paused, resumed and stopped. More over, split times can be taken too. The 

114 measurement is based on :func:`time.perf_counter_ns`. Additionally, starting and stopping is preserved as absolute 

115 time via :meth:`datetime.datetime.now`. 

116 

117 Every split time taken is a time delta to the previous operation. These are preserved in an internal sequence of 

118 splits. This sequence includes time deltas of activity and inactivity. Thus, a running stopwatch can be split as well 

119 as a paused stopwatch. 

120 

121 The stopwatch can also be used in a :ref:`with-statement <with>`, because it implements the :ref:`context manager protocol <context-managers>`. 

122 """ 

123 

124 _name: Nullable[str] #: Optional name of the stopwatch. 

125 _preferPause: bool #: If ``True``, the context manager pauses instead of stopping on exit. 

126 _digits: int #: Number of fractional digits ``__str__`` renders the duration with. 

127 

128 _beginTime: Nullable[datetime] #: Absolute time when the stopwatch was started. 

129 _endTime: Nullable[datetime] #: Absolute time when the stopwatch was stopped. 

130 _startTime: Nullable[int] #: Performance counter in ns when the stopwatch was started. 

131 _resumeTime: Nullable[int] #: Performance counter in ns of the latest resume operation. 

132 _pauseTime: Nullable[int] #: Performance counter in ns of the latest pause operation. 

133 _stopTime: Nullable[int] #: Performance counter in ns when the stopwatch was stopped. 

134 _totalTime: Nullable[int] #: Duration in ns from starting to stopping, activity and inactivity. 

135 _duration: Nullable[timedelta] #: :attr:`Duration` of a stopped stopwatch, computed on first access. 

136 _splits: list[tuple[float, bool]] #: Split times as (duration, is-active) pairs, in the order they were taken. 

137 

138 _excludeContextManager: ExcludeContextManager #: The nested context manager excluding time spans from measurement. 

139 

140 def __init__( 

141 self, 

142 name: Nullable[str] = None, 

143 started: bool = False, 

144 preferPause: bool = False, 

145 digits: int = 3 

146 ) -> None: 

147 """ 

148 Initializes the fields of the stopwatch. 

149 

150 If parameter ``started`` is set to true, the stopwatch will immediately start. 

151 

152 :param name: Optional, name of the stopwatch. 

153 :param started: Optional, if ``True``, start the stopwatch immediately. 

154 :param preferPause: Optional, if ``True``, ``__exit__(...)`` prefers pause over stop behavior. 

155 :param digits: Optional, number of fractional digits :meth:`__str__` renders the duration with. 

156 :raises TypeError: If parameter 'digits' is not of type :class:`int`. 

157 :raises ValueError: If parameter 'digits' is negative or greater than 9. 

158 """ 

159 if not isinstance(digits, int): 

160 ex = TypeError("Parameter 'digits' is not of type 'int'.") 

161 ex.add_note(f"Got type '{getFullyQualifiedName(digits)}'.") 

162 raise ex 

163 elif not 0 <= digits <= 9: 

164 ex = ValueError(f"Parameter 'digits' is out of range 0..9. Got {digits}.") 

165 ex.add_note("A duration in seconds has at most 9 digits (nanoseconds).") 

166 raise ex 

167 

168 self._name = name 

169 self._preferPause = preferPause 

170 self._digits = digits 

171 

172 self._endTime = None 

173 self._pauseTime = None 

174 self._stopTime = None 

175 self._totalTime = None 

176 self._duration = None 

177 self._splits = [] 

178 

179 self._excludeContextManager = None 

180 

181 if started is False: 

182 self._beginTime = None 

183 self._startTime = None 

184 self._resumeTime = None 

185 else: 

186 self._beginTime = datetime.now() 

187 self._resumeTime = self._startTime = perf_counter_ns() 

188 

189 def Start(self) -> None: 

190 """ 

191 Start the stopwatch. 

192 

193 A stopwatch can only be started once. There is no restart or reset operation provided. 

194 

195 :raises StopwatchError: If stopwatch was already started. 

196 :raises StopwatchError: If stopwatch was already started and stopped. 

197 """ 

198 if self._startTime is not None: 

199 raise StopwatchError("Stopwatch was already started.") 

200 

201 if self._stopTime is not None: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true

202 raise StopwatchError("Stopwatch was already used (started and stopped).") 

203 

204 self._beginTime = datetime.now() 

205 self._resumeTime = self._startTime = perf_counter_ns() 

206 

207 def Split(self) -> float: 

208 """ 

209 Take a split time and return the time delta to the previous stopwatch operation. 

210 

211 The stopwatch needs to be running to take a split time. See property :data:`IsRunning` to check if the stopwatch 

212 is running and the split operation is possible. |br| 

213 Depending on the previous operation, the time delta will be: 

214 

215 * the duration from start operation to the first split. 

216 * the duration from last resume to this split. 

217 

218 :returns: Duration in seconds since last stopwatch operation 

219 :raises StopwatchError: If stopwatch was not started or resumed. 

220 """ 

221 pauseTime = perf_counter_ns() 

222 

223 if self._resumeTime is None: 

224 raise StopwatchError("Stopwatch was not started or resumed.") 

225 

226 diff = (pauseTime - self._resumeTime) / 1e9 

227 self._splits.append((diff, True)) 

228 self._resumeTime = pauseTime 

229 

230 return diff 

231 

232 def Pause(self) -> float: 

233 """ 

234 Pause the stopwatch and return the time delta to the previous stopwatch operation. 

235 

236 The stopwatch needs to be running to pause it. See property :data:`IsRunning` to check if the stopwatch is running 

237 and the pause operation is possible. |br| 

238 Depending on the previous operation, the time delta will be: 

239 

240 * the duration from start operation to the first pause. 

241 * the duration from last resume to this pause. 

242 

243 :returns: Duration in seconds since last stopwatch operation 

244 :raises StopwatchError: If stopwatch was not started or resumed. 

245 """ 

246 self._pauseTime = perf_counter_ns() 

247 

248 if self._resumeTime is None: 

249 raise StopwatchError("Stopwatch was not started or resumed.") 

250 

251 diff = (self._pauseTime - self._resumeTime) / 1e9 

252 self._splits.append((diff, True)) 

253 self._resumeTime = None 

254 

255 return diff 

256 

257 def Resume(self) -> float: 

258 """ 

259 Resume the stopwatch and return the time delta to the previous pause operation. 

260 

261 The stopwatch needs to be paused to resume it. See property :data:`IsPaused` to check if the stopwatch is paused 

262 and the resume operation is possible. |br| 

263 The time delta will be the duration from last pause to this resume. 

264 

265 :returns: Duration in seconds since last pause operation 

266 :raises StopwatchError: If stopwatch was not paused. 

267 """ 

268 self._resumeTime = perf_counter_ns() 

269 

270 if self._pauseTime is None: 

271 raise StopwatchError("Stopwatch was not paused.") 

272 

273 diff = (self._resumeTime - self._pauseTime) / 1e9 

274 self._splits.append((diff, False)) 

275 self._pauseTime = None 

276 

277 return diff 

278 

279 def Stop(self) -> float: 

280 """ 

281 Stop the stopwatch and return the time delta to the previous stopwatch operation. 

282 

283 The stopwatch needs to be started to stop it. See property :data:`IsStarted` to check if the stopwatch was started 

284 and the stop operation is possible. |br| 

285 Depending on the previous operation, the time delta will be: 

286 

287 * the duration from start operation to the stop operation. 

288 * the duration from last resume to the stop operation. 

289 

290 :returns: Duration in seconds since last stopwatch operation 

291 :raises StopwatchError: If stopwatch was not started. 

292 :raises StopwatchError: If stopwatch was already stopped. 

293 """ 

294 self._stopTime = perf_counter_ns() 

295 self._endTime = datetime.now() 

296 

297 if self._startTime is None: 

298 raise StopwatchError("Stopwatch was never started.") 

299 

300 if self._totalTime is not None: 300 ↛ 301line 300 didn't jump to line 301 because the condition on line 300 was never true

301 raise StopwatchError("Stopwatch was already stopped.") 

302 

303 if len(self._splits) == 0: # was never paused 

304 diff = (self._stopTime - self._startTime) / 1e9 

305 elif self._resumeTime is None: # is paused 

306 diff = (self._stopTime - self._pauseTime) / 1e9 

307 self._splits.append((diff, False)) 

308 else: # is running 

309 diff = (self._stopTime - self._resumeTime) / 1e9 

310 self._splits.append((diff, True)) 

311 

312 self._pauseTime = None 

313 self._resumeTime = None 

314 self._totalTime = self._stopTime - self._startTime 

315 

316 # FIXME: why is this unused? 

317 beginEndDiff = self._endTime - self._beginTime 

318 

319 return diff 

320 

321 @property 

322 def Digits(self) -> int: 

323 """ 

324 Property to get and set the number of fractional digits (:attr:`_digits`) used by :meth:`__str__`. 

325 

326 The measurement itself is unaffected - this only decides how many digits of the duration in seconds are 

327 rendered. It defaults to ``3``, which is milliseconds. 

328 

329 :returns: Number of fractional digits. 

330 :raises TypeError: If the assigned value is not of type :class:`int`. 

331 :raises ValueError: If the assigned value is negative or greater than 9. 

332 """ 

333 return self._digits 

334 

335 @Digits.setter 

336 def Digits(self, digits: int) -> None: 

337 if not isinstance(digits, int): 

338 ex = TypeError("Parameter 'digits' is not of type 'int'.") 

339 ex.add_note(f"Got type '{getFullyQualifiedName(digits)}'.") 

340 raise ex 

341 elif not 0 <= digits <= 9: 

342 ex = ValueError(f"Parameter 'digits' is out of range 0..9. Got {digits}.") 

343 ex.add_note("A duration in seconds has at most 9 digits (nanoseconds).") 

344 raise ex 

345 

346 self._digits = digits 

347 

348 @readonly 

349 def Name(self) -> Nullable[str]: 

350 """ 

351 Read-only property returning the name of the stopwatch. 

352 

353 :returns: Name of the stopwatch. 

354 """ 

355 return self._name 

356 

357 @readonly 

358 def IsStarted(self) -> bool: 

359 """ 

360 Read-only property returning the IsStarted state of the stopwatch. 

361 

362 :returns: True, if stopwatch was started. 

363 """ 

364 return self._startTime is not None and self._stopTime is None 

365 

366 @readonly 

367 def IsRunning(self) -> bool: 

368 """ 

369 Read-only property returning the IsRunning state of the stopwatch. 

370 

371 :returns: True, if stopwatch was started and is currently not paused. 

372 """ 

373 return self._startTime is not None and self._resumeTime is not None 

374 

375 @readonly 

376 def IsPaused(self) -> bool: 

377 """ 

378 Read-only property returning the IsPaused state of the stopwatch. 

379 

380 :returns: True, if stopwatch was started and is currently paused. 

381 """ 

382 return self._startTime is not None and self._pauseTime is not None 

383 

384 @readonly 

385 def IsStopped(self) -> bool: 

386 """ 

387 Read-only property returning the IsStopped state of the stopwatch. 

388 

389 :returns: True, if stopwatch was stopped. 

390 """ 

391 return self._stopTime is not None 

392 

393 @readonly 

394 def StartTime(self) -> Nullable[datetime]: 

395 """ 

396 Read-only property returning the absolute time when the stopwatch was started. 

397 

398 :returns: The time when the stopwatch was started, otherwise None. 

399 """ 

400 return self._beginTime 

401 

402 @readonly 

403 def StopTime(self) -> Nullable[datetime]: 

404 """ 

405 Read-only property returning the absolute time when the stopwatch was stopped. 

406 

407 :returns: The time when the stopwatch was stopped, otherwise None. 

408 """ 

409 return self._endTime 

410 

411 @readonly 

412 def HasSplitTimes(self) -> bool: 

413 """ 

414 Read-only property checking if split times have been taken. 

415 

416 :returns: True, if at least one split time has been taken. 

417 """ 

418 return len(self._splits) > 0 

419 

420 @readonly 

421 def SplitCount(self) -> int: 

422 """ 

423 Read-only property returning the number of split times. 

424 

425 :returns: Number of split times. 

426 """ 

427 return len(self._splits) 

428 

429 @readonly 

430 def ActiveCount(self) -> int: 

431 """ 

432 Read-only property returning the number of active split times. 

433 

434 A running stopwatch is inside an active span that hasn't been recorded yet, and that span is counted here - 

435 the result is what the stopwatch would report if it were stopped right now. This matches 

436 :attr:`Activity`, which includes the running span's duration. 

437 

438 :returns: Number of active split times, including the one in progress. 

439 """ 

440 if self._startTime is None: 

441 return 0 

442 

443 return len([t for t, a in self._splits if a is True]) + (1 if self._resumeTime is not None else 0) 

444 

445 @readonly 

446 def InactiveCount(self) -> int: 

447 """ 

448 Read-only property returning the number of inactive split times. 

449 

450 A paused stopwatch is inside an inactive span that hasn't been recorded yet, and that span is counted here - 

451 the result is what the stopwatch would report if it were stopped right now. This matches 

452 :attr:`Inactivity`, which includes the paused span's duration. 

453 

454 :returns: Number of inactive split times, including the one in progress. 

455 """ 

456 if self._startTime is None: 

457 return 0 

458 

459 return len([t for t, a in self._splits if a is False]) + (1 if self._pauseTime is not None else 0) 

460 

461 @readonly 

462 def Activity(self) -> timedelta: 

463 """ 

464 Read-only property returning the duration of all active split times. 

465 

466 If the stopwatch is currently running, the duration since start or last resume operation will be included. A 

467 :class:`~datetime.timedelta` resolves microseconds; :attr:`ActivityInSeconds` is the same value as a float. 

468 

469 :returns: Duration of all active split times. If the stopwatch was never started, the return value is a zero 

470 duration. 

471 """ 

472 return timedelta(seconds=self.ActivityInSeconds) 

473 

474 @readonly 

475 def ActivityInSeconds(self) -> float: 

476 """ 

477 Read-only property returning the same duration as :attr:`Activity`, but in seconds. 

478 

479 :returns: Duration of all active split times in seconds. If the stopwatch was never started, the return value will 

480 be 0.0. 

481 """ 

482 if self._startTime is None: 

483 return 0.0 

484 

485 currentDiff = 0.0 if self._resumeTime is None else ((perf_counter_ns() - self._resumeTime) / 1e9) 

486 return sum(t for t, a in self._splits if a is True) + currentDiff 

487 

488 @readonly 

489 def Inactivity(self) -> timedelta: 

490 """ 

491 Read-only property returning the duration of all inactive split times. 

492 

493 If the stopwatch is currently paused, the duration since last pause operation will be included. A 

494 :class:`~datetime.timedelta` resolves microseconds; :attr:`InactivityInSeconds` is the same value as a float. 

495 

496 :returns: Duration of all inactive split times. If the stopwatch was never started, the return value is a zero 

497 duration. 

498 """ 

499 return timedelta(seconds=self.InactivityInSeconds) 

500 

501 @readonly 

502 def InactivityInSeconds(self) -> float: 

503 """ 

504 Read-only property returning the same duration as :attr:`Inactivity`, but in seconds. 

505 

506 :returns: Duration of all inactive split times in seconds. If the stopwatch was never started, the return value will 

507 be 0.0. 

508 """ 

509 if self._startTime is None: 

510 return 0.0 

511 

512 currentDiff = 0.0 if self._pauseTime is None else ((perf_counter_ns() - self._pauseTime) / 1e9) 

513 return sum(t for t, a in self._splits if a is False) + currentDiff 

514 

515 @readonly 

516 def Duration(self) -> timedelta: 

517 """ 

518 Read-only property returning the duration from start operation to stop operation. 

519 

520 If the stopwatch is not yet stopped, the duration from start to now is returned. A :class:`~datetime.timedelta` 

521 formats as hours, minutes and seconds and resolves microseconds; :attr:`DurationInSeconds` and 

522 :attr:`DurationInNanoseconds` are the same measurement as numbers. The duration of a stopped stopwatch is 

523 computed once (:attr:`_duration`). 

524 

525 :returns: Duration since the stopwatch was started. If the stopwatch was never started, the return value is a zero 

526 duration. 

527 """ 

528 if self._duration is not None: 

529 return self._duration 

530 

531 duration = timedelta(microseconds=self.DurationInNanoseconds / 1000) 

532 if self._totalTime is not None: 

533 self._duration = duration 

534 

535 return duration 

536 

537 @readonly 

538 def DurationInSeconds(self) -> float: 

539 """ 

540 Read-only property returning the same duration as :attr:`Duration`, but in seconds. 

541 

542 :returns: Duration since stopwatch was started in seconds. If the stopwatch was never started, the return value will 

543 be 0.0. 

544 """ 

545 return self.DurationInNanoseconds / 1e9 

546 

547 @readonly 

548 def DurationInNanoseconds(self) -> int: 

549 """ 

550 Read-only property returning the same duration as :attr:`Duration`, but in whole nanoseconds. 

551 

552 This is the measurement as the underlying :func:`time.perf_counter_ns` took it, so anything that divides a 

553 duration into parts - :meth:`__format__` does - works from an integer instead of converting a float back. 

554 

555 Precision is not the reason to prefer it. A float holds a duration in seconds exactly, to the nanosecond, up 

556 to :math:`2^{53}` ns - a little over 104 days - which no stopwatch will reach. 

557 

558 :returns: Duration since the stopwatch was started in nanoseconds. If the stopwatch was never started, the 

559 return value will be 0. 

560 """ 

561 if self._startTime is None: 

562 return 0 

563 elif self._totalTime is not None: # was stopped, so the total is final 

564 return self._totalTime 

565 

566 return perf_counter_ns() - self._startTime 

567 

568 @readonly 

569 def Exclude(self) -> ExcludeContextManager: 

570 """ 

571 Return an *exclude* context manager for the stopwatch instance. 

572 

573 :returns: An excluding context manager. 

574 """ 

575 if self._excludeContextManager is None: 

576 self._excludeContextManager = ExcludeContextManager(self) 

577 

578 return self._excludeContextManager 

579 

580 def __enter__(self) -> Self: 

581 """ 

582 Enter the context and start or resume the stopwatch. 

583 

584 An unstarted stopwatch is started, a paused one is resumed. 

585 

586 :returns: The stopwatch itself. 

587 :raises StopwatchError: If the stopwatch is running. 

588 :raises StopwatchError: If the stopwatch was already stopped. 

589 """ 

590 if self._startTime is None: # start stopwatch 

591 self._beginTime = datetime.now() 

592 self._resumeTime = self._startTime = perf_counter_ns() 

593 elif self._pauseTime is not None: # resume after pause 

594 self._resumeTime = perf_counter_ns() 

595 

596 diff = (self._resumeTime - self._pauseTime) / 1e9 

597 self._splits.append((diff, False)) 

598 self._pauseTime = None 

599 elif self._resumeTime is not None: # is running? 

600 raise StopwatchError("Stopwatch is currently running and can not be started/resumed again.") 

601 elif self._stopTime is not None: # is stopped? 601 ↛ 604line 601 didn't jump to line 604 because the condition on line 601 was always true

602 raise StopwatchError("Stopwatch was already stopped.") 

603 else: 

604 raise StopwatchError("Internal error.") 

605 

606 return self 

607 

608 def __exit__( 

609 self, 

610 exc_type: Nullable[type[BaseException]] = None, 

611 exc_val: Nullable[BaseException] = None, 

612 exc_tb: Nullable[TracebackType] = None 

613 ) -> Nullable[bool]: 

614 """ 

615 Exit the context and pause or stop the stopwatch. 

616 

617 A running stopwatch is paused, if ``preferPause`` was set, otherwise it's stopped. 

618 

619 :param exc_type: Optional, exception type, otherwise None. 

620 :param exc_val: Optional, exception object, otherwise None. 

621 :param exc_tb: Optional, exception's traceback, otherwise None. 

622 :returns: ``None`` 

623 :raises StopwatchError: If the stopwatch was never started. 

624 :raises StopwatchError: If the stopwatch was already stopped. 

625 :raises StopwatchError: If the stopwatch is paused, because it wasn't resumed. 

626 """ 

627 if self._startTime is None: # never started? 

628 raise StopwatchError("Stopwatch was never started.") 

629 elif self._stopTime is not None: 

630 raise StopwatchError("Stopwatch was already stopped.") 

631 elif self._resumeTime is not None: # pause or stop 

632 if self._preferPause: 

633 self._pauseTime = perf_counter_ns() 

634 diff = (self._pauseTime - self._resumeTime) / 1e9 

635 self._splits.append((diff, True)) 

636 self._resumeTime = None 

637 else: 

638 self._stopTime = perf_counter_ns() 

639 self._endTime = datetime.now() 

640 

641 diff = (self._stopTime - self._resumeTime) / 1e9 

642 self._splits.append((diff, True)) 

643 

644 self._pauseTime = None 

645 self._resumeTime = None 

646 self._totalTime = self._stopTime - self._startTime 

647 else: 

648 raise StopwatchError("Stopwatch was not resumed.") 

649 

650 def __len__(self) -> int: 

651 """ 

652 Implementation of ``len(...)`` to return the number of split times. 

653 

654 :returns: Number of split times. 

655 """ 

656 return len(self._splits) 

657 

658 def __getitem__(self, index: int) -> tuple[float, bool]: 

659 """ 

660 Implementation of ``split = object[i]`` to return the i-th split time. 

661 

662 :param index: Index to access the i-th split time. 

663 :returns: i-th split time as a tuple of: |br| 

664 (1) delta time to the previous stopwatch operation and |br| 

665 (2) a boolean indicating if the split was an activity (true) or inactivity (false). 

666 :raises IndexError: If index *i* doesn't exist. 

667 """ 

668 return self._splits[index] 

669 

670 def __iter__(self) -> Iterator[tuple[float, bool]]: 

671 """ 

672 Return an iterator of tuples to iterate all split times. 

673 

674 If the stopwatch is not stopped yet, the last split won't be included. 

675 

676 :returns: Iterator of split time tuples of: |br| 

677 (1) delta time to the previous stopwatch operation and |br| 

678 (2) a boolean indicating if the split was an activity (true) or inactivity (false). 

679 """ 

680 return self._splits.__iter__() 

681 

682 def __format__(self, formatSpec: str) -> str: 

683 """ 

684 Return the measured duration according to the format specification. 

685 

686 .. topic:: Format Specifiers 

687 

688 An **uppercase** specifier is a field of the duration as it would be displayed. A **lowercase** specifier is 

689 the whole duration expressed in one unit, which is what a report or a comparison wants. 

690 

691 +-----------+--------------------------------------------------------+ 

692 | Specifier | Meaning | 

693 +===========+========================================================+ 

694 | ``%H`` | hours, not capped - a 26 hour measurement shows ``26`` | 

695 +-----------+--------------------------------------------------------+ 

696 | ``%M`` | minutes, ``00`` to ``59`` | 

697 +-----------+--------------------------------------------------------+ 

698 | ``%S`` | seconds, ``00`` to ``59`` | 

699 +-----------+--------------------------------------------------------+ 

700 | ``%L`` | fractional seconds, 3 digits (milliseconds) | 

701 +-----------+--------------------------------------------------------+ 

702 | ``%U`` | fractional seconds, 6 digits (microseconds) | 

703 +-----------+--------------------------------------------------------+ 

704 | ``%N`` | fractional seconds, 9 digits (nanoseconds) | 

705 +-----------+--------------------------------------------------------+ 

706 | ``%s`` | the whole duration in seconds | 

707 +-----------+--------------------------------------------------------+ 

708 | ``%m`` | the whole duration in milliseconds | 

709 +-----------+--------------------------------------------------------+ 

710 | ``%u`` | the whole duration in microseconds | 

711 +-----------+--------------------------------------------------------+ 

712 | ``%n`` | the whole duration in nanoseconds | 

713 +-----------+--------------------------------------------------------+ 

714 

715 The fractional specifiers are truncations of the same fraction, so ``%S.%U`` renders ``04.123456`` without 

716 having to be combined with anything. ``%H`` is not capped, so ``%H:%M:%S`` never silently drops a day. 

717 

718 ``%%`` renders a literal percent sign. An empty format specification returns :meth:`__str__`. 

719 

720 :param formatSpec: The format specification, using ``%``-placeholders for the duration's parts. 

721 :returns: The formatted duration. 

722 :raises ValueError: If the format specification contains an unknown placeholder. 

723 """ 

724 if formatSpec == "": 

725 return self.__str__() 

726 

727 nanoseconds = self.DurationInNanoseconds 

728 seconds, fraction = divmod(nanoseconds, 1_000_000_000) 

729 minutes, secondField = divmod(seconds, 60) 

730 hours, minuteField = divmod(minutes, 60) 

731 

732 result = formatSpec 

733 for placeholder, value in ( 

734 ("%H", f"{hours:02}"), 

735 ("%M", f"{minuteField:02}"), 

736 ("%S", f"{secondField:02}"), 

737 ("%L", f"{fraction // 1_000_000:03}"), 

738 ("%U", f"{fraction // 1_000:06}"), 

739 ("%N", f"{fraction:09}"), 

740 ("%s", f"{nanoseconds // 1_000_000_000}"), 

741 ("%m", f"{nanoseconds // 1_000_000}"), 

742 ("%u", f"{nanoseconds // 1_000}"), 

743 ("%n", f"{nanoseconds}"), 

744 ): 

745 result = result.replace(placeholder, value) 

746 

747 if (position := result.find("%")) != -1: 

748 following = result[position + 1] if position + 1 < len(result) else "" 

749 if following != "%": 

750 raise ValueError(f"Unknown format specifier '%{following}' in '{formatSpec}'.") 

751 

752 return result.replace("%%", "%") 

753 

754 def __str__(self) -> str: 

755 """ 

756 Returns the stopwatch's state and its measured time span. 

757 

758 The duration is rendered in seconds with :attr:`Digits` fractional digits, in every state - a running and a 

759 stopped stopwatch report the same unit at the same resolution. 

760 

761 :returns: The string equivalent of the stopwatch. 

762 """ 

763 name = f" {self._name}" if self._name is not None else "" 

764 duration = f"{self.DurationInSeconds:.{self._digits}f}" 

765 

766 if self.IsStopped: 

767 return f"Stopwatch{name} (stopped): {self._beginTime} -> {self._endTime}: {duration}" 

768 elif self.IsRunning: 

769 return f"Stopwatch{name} (running): {self._beginTime} -> now: {duration}" 

770 elif self.IsPaused: 

771 return f"Stopwatch{name} (paused): {self._beginTime} -> now: {duration}" 

772 else: 

773 return f"Stopwatch{name}: not started"