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

252 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-08 20:44 +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 

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: Exception type 

101 :param exc_val: Exception instance 

102 :param exc_tb: 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 _splits: list[tuple[float, bool]] #: Split times as (duration, is-active) pairs, in the order they were taken. 

136 

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

138 

139 def __init__( 

140 self, 

141 name: Nullable[str] = None, 

142 started: bool = False, 

143 preferPause: bool = False, 

144 digits: int = 3 

145 ) -> None: 

146 """ 

147 Initializes the fields of the stopwatch. 

148 

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

150 

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

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

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

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

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

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

157 """ 

158 if not isinstance(digits, int): 

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

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

161 raise ex 

162 elif not 0 <= digits <= 9: 

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

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

165 raise ex 

166 

167 self._name = name 

168 self._preferPause = preferPause 

169 self._digits = digits 

170 

171 self._endTime = None 

172 self._pauseTime = None 

173 self._stopTime = None 

174 self._totalTime = None 

175 self._splits = [] 

176 

177 self._excludeContextManager = None 

178 

179 if started is False: 

180 self._beginTime = None 

181 self._startTime = None 

182 self._resumeTime = None 

183 else: 

184 self._beginTime = datetime.now() 

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

186 

187 def Start(self) -> None: 

188 """ 

189 Start the stopwatch. 

190 

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

192 

193 :raises StopwatchError: If stopwatch was already started. 

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

195 """ 

196 if self._startTime is not None: 

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

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

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

200 

201 self._beginTime = datetime.now() 

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

203 

204 def Split(self) -> float: 

205 """ 

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

207 

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

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

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

211 

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

213 * the duration from last resume to this split. 

214 

215 :returns: Duration in seconds since last stopwatch operation 

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

217 """ 

218 pauseTime = perf_counter_ns() 

219 

220 if self._resumeTime is None: 

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

222 

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

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

225 self._resumeTime = pauseTime 

226 

227 return diff 

228 

229 def Pause(self) -> float: 

230 """ 

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

232 

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

234 and the pause operation is possible. |br| 

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

236 

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

238 * the duration from last resume to this pause. 

239 

240 :returns: Duration in seconds since last stopwatch operation 

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

242 """ 

243 self._pauseTime = perf_counter_ns() 

244 

245 if self._resumeTime is None: 

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

247 

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

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

250 self._resumeTime = None 

251 

252 return diff 

253 

254 def Resume(self) -> float: 

255 """ 

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

257 

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

259 and the resume operation is possible. |br| 

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

261 

262 :returns: Duration in seconds since last pause operation 

263 :raises StopwatchError: If stopwatch was not paused. 

264 """ 

265 self._resumeTime = perf_counter_ns() 

266 

267 if self._pauseTime is None: 

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

269 

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

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

272 self._pauseTime = None 

273 

274 return diff 

275 

276 def Stop(self) -> float: 

277 """ 

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

279 

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

281 and the stop operation is possible. |br| 

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

283 

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

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

286 

287 :returns: Duration in seconds since last stopwatch operation 

288 :raises StopwatchError: If stopwatch was not started. 

289 :raises StopwatchError: If stopwatch was already stopped. 

290 """ 

291 self._stopTime = perf_counter_ns() 

292 self._endTime = datetime.now() 

293 

294 if self._startTime is None: 

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

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

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

298 

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

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

301 elif self._resumeTime is None: # is paused 

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

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

304 else: # is running 

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

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

307 

308 self._pauseTime = None 

309 self._resumeTime = None 

310 self._totalTime = self._stopTime - self._startTime 

311 

312 # FIXME: why is this unused? 

313 beginEndDiff = self._endTime - self._beginTime 

314 

315 return diff 

316 

317 @property 

318 def Digits(self) -> int: 

319 """ 

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

321 

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

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

324 

325 :returns: Number of fractional digits. 

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

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

328 """ 

329 return self._digits 

330 

331 @Digits.setter 

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

333 if not isinstance(digits, int): 

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

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

336 raise ex 

337 elif not 0 <= digits <= 9: 

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

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

340 raise ex 

341 

342 self._digits = digits 

343 

344 @readonly 

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

346 """ 

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

348 

349 :returns: Name of the stopwatch. 

350 """ 

351 return self._name 

352 

353 @readonly 

354 def IsStarted(self) -> bool: 

355 """ 

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

357 

358 :returns: True, if stopwatch was started. 

359 """ 

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

361 

362 @readonly 

363 def IsRunning(self) -> bool: 

364 """ 

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

366 

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

368 """ 

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

370 

371 @readonly 

372 def IsPaused(self) -> bool: 

373 """ 

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

375 

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

377 """ 

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

379 

380 @readonly 

381 def IsStopped(self) -> bool: 

382 """ 

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

384 

385 :returns: True, if stopwatch was stopped. 

386 """ 

387 return self._stopTime is not None 

388 

389 @readonly 

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

391 """ 

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

393 

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

395 """ 

396 return self._beginTime 

397 

398 @readonly 

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

400 """ 

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

402 

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

404 """ 

405 return self._endTime 

406 

407 @readonly 

408 def HasSplitTimes(self) -> bool: 

409 """ 

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

411 

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

413 """ 

414 return len(self._splits) > 0 

415 

416 @readonly 

417 def SplitCount(self) -> int: 

418 """ 

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

420 

421 :returns: Number of split times. 

422 """ 

423 return len(self._splits) 

424 

425 @readonly 

426 def ActiveCount(self) -> int: 

427 """ 

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

429 

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

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

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

433 

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

435 """ 

436 if self._startTime is None: 

437 return 0 

438 

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

440 

441 @readonly 

442 def InactiveCount(self) -> int: 

443 """ 

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

445 

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

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

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

449 

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

451 """ 

452 if self._startTime is None: 

453 return 0 

454 

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

456 

457 @readonly 

458 def Activity(self) -> float: 

459 """ 

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

461 

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

463 

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

465 be 0.0. 

466 """ 

467 if self._startTime is None: 467 ↛ 468line 467 didn't jump to line 468 because the condition on line 467 was never true

468 return 0.0 

469 

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

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

472 

473 @readonly 

474 def Inactivity(self) -> float: 

475 """ 

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

477 

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

479 

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

481 be 0.0. 

482 """ 

483 if self._startTime is None: 483 ↛ 484line 483 didn't jump to line 484 because the condition on line 483 was never true

484 return 0.0 

485 

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

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

488 

489 @readonly 

490 def Duration(self) -> float: 

491 """ 

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

493 

494 If the stopwatch is not yet stopped, the duration from start to now is returned. 

495 

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

497 be 0.0. 

498 """ 

499 return self.DurationInNanoseconds / 1e9 

500 

501 @readonly 

502 def DurationInNanoseconds(self) -> int: 

503 """ 

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

505 

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

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

508 

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

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

511 

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

513 return value will be 0. 

514 """ 

515 if self._startTime is None: 

516 return 0 

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

518 return self._totalTime 

519 

520 return perf_counter_ns() - self._startTime 

521 

522 @readonly 

523 def Exclude(self) -> ExcludeContextManager: 

524 """ 

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

526 

527 :returns: An excluding context manager. 

528 """ 

529 if self._excludeContextManager is None: 

530 self._excludeContextManager = ExcludeContextManager(self) 

531 

532 return self._excludeContextManager 

533 

534 def __enter__(self) -> Self: 

535 """ 

536 Implementation of the :ref:`context manager protocol's <context-managers>` ``__enter__(...)`` method. 

537 

538 An unstarted stopwatch will be started. A paused stopwatch will be resumed. 

539 

540 :returns: The stopwatch itself. 

541 :raises StopwatchError: If the stopwatch was already started. 

542 """ 

543 if self._startTime is None: # start stopwatch 

544 self._beginTime = datetime.now() 

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

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

547 self._resumeTime = perf_counter_ns() 

548 

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

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

551 self._pauseTime = None 

552 elif self._resumeTime is not None: # is running? 552 ↛ 553line 552 didn't jump to line 553 because the condition on line 552 was never true

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

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

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

556 else: 

557 raise StopwatchError("Internal error.") 

558 

559 return self 

560 

561 def __exit__( 

562 self, 

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

564 exc_val: Nullable[BaseException] = None, 

565 exc_tb: Nullable[TracebackType] = None 

566 ) -> Nullable[bool]: 

567 """ 

568 Implementation of the :ref:`context manager protocol's <context-managers>` ``__exit__(...)`` method. 

569 

570 A running stopwatch will be paused or stopped depending on the configured ``preferPause`` behavior. 

571 

572 :param exc_type: Exception type, otherwise None. 

573 :param exc_val: Exception object, otherwise None. 

574 :param exc_tb: Exception's traceback, otherwise None. 

575 :returns: True, if exceptions should be suppressed. 

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

577 """ 

578 if self._startTime is None: # never started? 578 ↛ 579line 578 didn't jump to line 579 because the condition on line 578 was never true

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

580 elif self._stopTime is not None: 580 ↛ 581line 580 didn't jump to line 581 because the condition on line 580 was never true

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

582 elif self._resumeTime is not None: # pause or stop 582 ↛ 599line 582 didn't jump to line 599 because the condition on line 582 was always true

583 if self._preferPause: 

584 self._pauseTime = perf_counter_ns() 

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

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

587 self._resumeTime = None 

588 else: 

589 self._stopTime = perf_counter_ns() 

590 self._endTime = datetime.now() 

591 

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

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

594 

595 self._pauseTime = None 

596 self._resumeTime = None 

597 self._totalTime = self._stopTime - self._startTime 

598 else: 

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

600 

601 def __len__(self) -> int: 

602 """ 

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

604 

605 :returns: Number of split times. 

606 """ 

607 return len(self._splits) 

608 

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

610 """ 

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

612 

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

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

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

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

617 :raises KeyError: If index *i* doesn't exist. 

618 """ 

619 return self._splits[index] 

620 

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

622 """ 

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

624 

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

626 

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

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

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

630 """ 

631 return self._splits.__iter__() 

632 

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

634 """ 

635 Return the measured duration according to the format specification. 

636 

637 .. topic:: Format Specifiers 

638 

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

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

641 

642 +-----------+--------------------------------------------------------+ 

643 | Specifier | Meaning | 

644 +===========+========================================================+ 

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

646 +-----------+--------------------------------------------------------+ 

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

648 +-----------+--------------------------------------------------------+ 

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

650 +-----------+--------------------------------------------------------+ 

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

652 +-----------+--------------------------------------------------------+ 

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

654 +-----------+--------------------------------------------------------+ 

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

656 +-----------+--------------------------------------------------------+ 

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

658 +-----------+--------------------------------------------------------+ 

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

660 +-----------+--------------------------------------------------------+ 

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

662 +-----------+--------------------------------------------------------+ 

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

664 +-----------+--------------------------------------------------------+ 

665 

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

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

668 

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

670 

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

672 :returns: The formatted duration. 

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

674 """ 

675 if formatSpec == "": 

676 return self.__str__() 

677 

678 nanoseconds = self.DurationInNanoseconds 

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

680 minutes, secondField = divmod(seconds, 60) 

681 hours, minuteField = divmod(minutes, 60) 

682 

683 result = formatSpec 

684 for placeholder, value in ( 

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

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

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

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

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

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

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

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

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

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

695 ): 

696 result = result.replace(placeholder, value) 

697 

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

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

700 if following != "%": 

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

702 

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

704 

705 def __str__(self) -> str: 

706 """ 

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

708 

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

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

711 

712 :returns: The string equivalent of the stopwatch. 

713 """ 

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

715 duration = f"{self.Duration:.{self._digits}f}" 

716 

717 if self.IsStopped: 

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

719 elif self.IsRunning: 

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

721 elif self.IsPaused: 

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

723 else: 

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