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

252 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-03 23:02 +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 

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

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

201 

202 self._beginTime = datetime.now() 

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

204 

205 def Split(self) -> float: 

206 """ 

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

208 

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

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

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

212 

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

214 * the duration from last resume to this split. 

215 

216 :returns: Duration in seconds since last stopwatch operation 

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

218 """ 

219 pauseTime = perf_counter_ns() 

220 

221 if self._resumeTime is None: 

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

223 

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

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

226 self._resumeTime = pauseTime 

227 

228 return diff 

229 

230 def Pause(self) -> float: 

231 """ 

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

233 

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

235 and the pause operation is possible. |br| 

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

237 

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

239 * the duration from last resume to this pause. 

240 

241 :returns: Duration in seconds since last stopwatch operation 

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

243 """ 

244 self._pauseTime = perf_counter_ns() 

245 

246 if self._resumeTime is None: 

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

248 

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

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

251 self._resumeTime = None 

252 

253 return diff 

254 

255 def Resume(self) -> float: 

256 """ 

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

258 

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

260 and the resume operation is possible. |br| 

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

262 

263 :returns: Duration in seconds since last pause operation 

264 :raises StopwatchError: If stopwatch was not paused. 

265 """ 

266 self._resumeTime = perf_counter_ns() 

267 

268 if self._pauseTime is None: 

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

270 

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

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

273 self._pauseTime = None 

274 

275 return diff 

276 

277 def Stop(self) -> float: 

278 """ 

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

280 

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

282 and the stop operation is possible. |br| 

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

284 

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

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

287 

288 :returns: Duration in seconds since last stopwatch operation 

289 :raises StopwatchError: If stopwatch was not started. 

290 :raises StopwatchError: If stopwatch was already stopped. 

291 """ 

292 self._stopTime = perf_counter_ns() 

293 self._endTime = datetime.now() 

294 

295 if self._startTime is None: 

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

297 

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

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

300 

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

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

303 elif self._resumeTime is None: # is paused 

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

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

306 else: # is running 

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

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

309 

310 self._pauseTime = None 

311 self._resumeTime = None 

312 self._totalTime = self._stopTime - self._startTime 

313 

314 # FIXME: why is this unused? 

315 beginEndDiff = self._endTime - self._beginTime 

316 

317 return diff 

318 

319 @property 

320 def Digits(self) -> int: 

321 """ 

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

323 

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

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

326 

327 :returns: Number of fractional digits. 

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

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

330 """ 

331 return self._digits 

332 

333 @Digits.setter 

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

335 if not isinstance(digits, int): 

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

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

338 raise ex 

339 elif not 0 <= digits <= 9: 

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

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

342 raise ex 

343 

344 self._digits = digits 

345 

346 @readonly 

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

348 """ 

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

350 

351 :returns: Name of the stopwatch. 

352 """ 

353 return self._name 

354 

355 @readonly 

356 def IsStarted(self) -> bool: 

357 """ 

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

359 

360 :returns: True, if stopwatch was started. 

361 """ 

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

363 

364 @readonly 

365 def IsRunning(self) -> bool: 

366 """ 

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

368 

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

370 """ 

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

372 

373 @readonly 

374 def IsPaused(self) -> bool: 

375 """ 

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

377 

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

379 """ 

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

381 

382 @readonly 

383 def IsStopped(self) -> bool: 

384 """ 

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

386 

387 :returns: True, if stopwatch was stopped. 

388 """ 

389 return self._stopTime is not None 

390 

391 @readonly 

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

393 """ 

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

395 

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

397 """ 

398 return self._beginTime 

399 

400 @readonly 

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

402 """ 

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

404 

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

406 """ 

407 return self._endTime 

408 

409 @readonly 

410 def HasSplitTimes(self) -> bool: 

411 """ 

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

413 

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

415 """ 

416 return len(self._splits) > 0 

417 

418 @readonly 

419 def SplitCount(self) -> int: 

420 """ 

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

422 

423 :returns: Number of split times. 

424 """ 

425 return len(self._splits) 

426 

427 @readonly 

428 def ActiveCount(self) -> int: 

429 """ 

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

431 

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

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

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

435 

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

437 """ 

438 if self._startTime is None: 

439 return 0 

440 

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

442 

443 @readonly 

444 def InactiveCount(self) -> int: 

445 """ 

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

447 

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

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

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

451 

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

453 """ 

454 if self._startTime is None: 

455 return 0 

456 

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

458 

459 @readonly 

460 def Activity(self) -> float: 

461 """ 

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

463 

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

465 

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

467 be 0.0. 

468 """ 

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

470 return 0.0 

471 

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

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

474 

475 @readonly 

476 def Inactivity(self) -> float: 

477 """ 

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

479 

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

481 

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

483 be 0.0. 

484 """ 

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

486 return 0.0 

487 

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

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

490 

491 @readonly 

492 def Duration(self) -> float: 

493 """ 

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

495 

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

497 

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

499 be 0.0. 

500 """ 

501 return self.DurationInNanoseconds / 1e9 

502 

503 @readonly 

504 def DurationInNanoseconds(self) -> int: 

505 """ 

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

507 

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

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

510 

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

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

513 

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

515 return value will be 0. 

516 """ 

517 if self._startTime is None: 

518 return 0 

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

520 return self._totalTime 

521 

522 return perf_counter_ns() - self._startTime 

523 

524 @readonly 

525 def Exclude(self) -> ExcludeContextManager: 

526 """ 

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

528 

529 :returns: An excluding context manager. 

530 """ 

531 if self._excludeContextManager is None: 

532 self._excludeContextManager = ExcludeContextManager(self) 

533 

534 return self._excludeContextManager 

535 

536 def __enter__(self) -> Self: 

537 """ 

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

539 

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

541 

542 :returns: The stopwatch itself. 

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

544 """ 

545 if self._startTime is None: # start stopwatch 

546 self._beginTime = datetime.now() 

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

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

549 self._resumeTime = perf_counter_ns() 

550 

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

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

553 self._pauseTime = None 

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

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

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

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

558 else: 

559 raise StopwatchError("Internal error.") 

560 

561 return self 

562 

563 def __exit__( 

564 self, 

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

566 exc_val: Nullable[BaseException] = None, 

567 exc_tb: Nullable[TracebackType] = None 

568 ) -> Nullable[bool]: 

569 """ 

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

571 

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

573 

574 :param exc_type: Exception type, otherwise None. 

575 :param exc_val: Exception object, otherwise None. 

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

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

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

579 """ 

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

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

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

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

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

585 if self._preferPause: 

586 self._pauseTime = perf_counter_ns() 

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

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

589 self._resumeTime = None 

590 else: 

591 self._stopTime = perf_counter_ns() 

592 self._endTime = datetime.now() 

593 

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

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

596 

597 self._pauseTime = None 

598 self._resumeTime = None 

599 self._totalTime = self._stopTime - self._startTime 

600 else: 

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

602 

603 def __len__(self) -> int: 

604 """ 

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

606 

607 :returns: Number of split times. 

608 """ 

609 return len(self._splits) 

610 

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

612 """ 

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

614 

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

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

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

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

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

620 """ 

621 return self._splits[index] 

622 

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

624 """ 

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

626 

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

628 

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

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

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

632 """ 

633 return self._splits.__iter__() 

634 

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

636 """ 

637 Return the measured duration according to the format specification. 

638 

639 .. topic:: Format Specifiers 

640 

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

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

643 

644 +-----------+--------------------------------------------------------+ 

645 | Specifier | Meaning | 

646 +===========+========================================================+ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

666 +-----------+--------------------------------------------------------+ 

667 

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

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

670 

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

672 

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

674 :returns: The formatted duration. 

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

676 """ 

677 if formatSpec == "": 

678 return self.__str__() 

679 

680 nanoseconds = self.DurationInNanoseconds 

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

682 minutes, secondField = divmod(seconds, 60) 

683 hours, minuteField = divmod(minutes, 60) 

684 

685 result = formatSpec 

686 for placeholder, value in ( 

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

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

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

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

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

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

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

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

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

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

697 ): 

698 result = result.replace(placeholder, value) 

699 

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

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

702 if following != "%": 

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

704 

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

706 

707 def __str__(self) -> str: 

708 """ 

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

710 

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

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

713 

714 :returns: The string equivalent of the stopwatch. 

715 """ 

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

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

718 

719 if self.IsStopped: 

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

721 elif self.IsRunning: 

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

723 elif self.IsPaused: 

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

725 else: 

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