executiontimer 1.0.3__tar.gz → 1.1.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.
Files changed (29) hide show
  1. {executiontimer-1.0.3 → executiontimer-1.1.0}/CHANGELOG.md +14 -1
  2. {executiontimer-1.0.3 → executiontimer-1.1.0}/PKG-INFO +6 -6
  3. {executiontimer-1.0.3 → executiontimer-1.1.0}/README.md +5 -5
  4. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/getting-started.md +11 -9
  5. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/counters.md +5 -5
  6. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/reports.md +12 -8
  7. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/timing-code.md +4 -4
  8. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/index.md +5 -5
  9. {executiontimer-1.0.3 → executiontimer-1.1.0}/src/execution_timer/__init__.py +1 -1
  10. {executiontimer-1.0.3 → executiontimer-1.1.0}/src/execution_timer/_timer.py +19 -4
  11. {executiontimer-1.0.3 → executiontimer-1.1.0}/tests/execution_timer_test.py +22 -3
  12. {executiontimer-1.0.3 → executiontimer-1.1.0}/.gitignore +0 -0
  13. {executiontimer-1.0.3 → executiontimer-1.1.0}/CONTRIBUTING.md +0 -0
  14. {executiontimer-1.0.3 → executiontimer-1.1.0}/LICENSE +0 -0
  15. {executiontimer-1.0.3 → executiontimer-1.1.0}/benchmarks/README.md +0 -0
  16. {executiontimer-1.0.3 → executiontimer-1.1.0}/benchmarks/overhead.py +0 -0
  17. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/_static/icon-dark.svg +0 -0
  18. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/_static/icon-light.svg +0 -0
  19. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/api.md +0 -0
  20. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/changelog.md +0 -0
  21. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/conf.py +0 -0
  22. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/contributing.md +0 -0
  23. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/categories.md +0 -0
  24. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/concurrency.md +0 -0
  25. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/long-running.md +0 -0
  26. {executiontimer-1.0.3 → executiontimer-1.1.0}/docs/guide/overhead.md +0 -0
  27. {executiontimer-1.0.3 → executiontimer-1.1.0}/pyproject.toml +0 -0
  28. {executiontimer-1.0.3 → executiontimer-1.1.0}/src/execution_timer/py.typed +0 -0
  29. {executiontimer-1.0.3 → executiontimer-1.1.0}/tests/docs_examples_test.py +0 -0
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-09-30
11
+
12
+ ### Changed
13
+
14
+ - The text report, from `get_execution_times_report()` and `log_execution_times()`, shows
15
+ each time with three decimals in `µs`, `ms` or `s`, whichever keeps the number below
16
+ 1000: `48.213 µs`, `315.612 ms`, `2.500 s`. It used to show seconds with four
17
+ decimals, so any section under 50 µs read `0.0000 s`, and fast sections, including a
18
+ whole recursive call tree, looked like zeros. Code that parses the report text needs
19
+ updating. `get_execution_timings()` and the JSON export are unchanged and still return
20
+ seconds.
21
+
10
22
  ## [1.0.3] - 2026-09-30
11
23
 
12
24
  ### Changed
@@ -194,7 +206,8 @@ First public release on PyPI.
194
206
  `py.typed` marker so type checkers use the inline annotations.
195
207
  - `__version__` attribute on the package.
196
208
 
197
- [Unreleased]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.3...HEAD
209
+ [Unreleased]: https://github.com/seba2390/ExecutionTimer/compare/v1.1.0...HEAD
210
+ [1.1.0]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.3...v1.1.0
198
211
  [1.0.3]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.2...v1.0.3
199
212
  [1.0.2]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.1...v1.0.2
200
213
  [1.0.1]: https://github.com/seba2390/ExecutionTimer/compare/v1.0.0...v1.0.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: executiontimer
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: Hierarchical execution timing with user-defined categories.
5
5
  Project-URL: Homepage, https://github.com/seba2390/ExecutionTimer
6
6
  Project-URL: Documentation, https://seba2390.github.io/ExecutionTimer/
@@ -84,12 +84,12 @@ print(get_execution_times_report())
84
84
  ```
85
85
 
86
86
  ```text
87
- Total time: 0.3156 s.
87
+ Total time: 312.838 ms.
88
88
 
89
- load_data: 0.1219 s (38.62%)
90
- solve: 0.1937 s (61.38%)
91
- .. step: 0.1585 s (50.24%)
92
- .. postprocess: 0.0350 s (11.10%)
89
+ load_data: 124.682 ms (39.85%)
90
+ solve: 188.157 ms (60.15%)
91
+ .. step: 154.718 ms (49.46%)
92
+ .. postprocess: 33.295 ms (10.64%)
93
93
  ```
94
94
 
95
95
  `TimerContext` also works as a decorator, including on `async def` functions:
@@ -53,12 +53,12 @@ print(get_execution_times_report())
53
53
  ```
54
54
 
55
55
  ```text
56
- Total time: 0.3156 s.
56
+ Total time: 312.838 ms.
57
57
 
58
- load_data: 0.1219 s (38.62%)
59
- solve: 0.1937 s (61.38%)
60
- .. step: 0.1585 s (50.24%)
61
- .. postprocess: 0.0350 s (11.10%)
58
+ load_data: 124.682 ms (39.85%)
59
+ solve: 188.157 ms (60.15%)
60
+ .. step: 154.718 ms (49.46%)
61
+ .. postprocess: 33.295 ms (10.64%)
62
62
  ```
63
63
 
64
64
  `TimerContext` also works as a decorator, including on `async def` functions:
@@ -42,9 +42,9 @@ print(get_execution_times_report())
42
42
  ```
43
43
 
44
44
  ```text
45
- Total time: 0.1035 s.
45
+ Total time: 104.727 ms.
46
46
 
47
- load_data: 0.1035 s (100.00%)
47
+ load_data: 104.727 ms (100.00%)
48
48
  ```
49
49
 
50
50
  Timings are recorded in one registry for the whole process. Every section you time, in any
@@ -72,12 +72,12 @@ print(get_execution_times_report())
72
72
  ```
73
73
 
74
74
  ```text
75
- Total time: 0.0905 s.
75
+ Total time: 89.826 ms.
76
76
 
77
- pipeline: 0.0905 s (100.00%)
78
- .. load: 0.0232 s (25.60%)
79
- .. transform: 0.0673 s (74.32%)
80
- .. .. validate: 0.0126 s (13.88%)
77
+ pipeline: 89.826 ms (100.00%)
78
+ .. load: 22.073 ms (24.57%)
79
+ .. transform: 67.726 ms (75.40%)
80
+ .. .. validate: 12.549 ms (13.97%)
81
81
  ```
82
82
 
83
83
  How to read the report:
@@ -86,8 +86,10 @@ How to read the report:
86
86
  `pipeline`.
87
87
  - **Total time** is the sum of the top-level sections. Nested sections are part of their
88
88
  parent's time, so they aren't added again.
89
- - **Percentages** are shares of the total, at every level. `validate` took 13.88% of the
90
- whole run, not 13.88% of `transform`.
89
+ - **Percentages** are shares of the total, at every level. `validate` took 13.97% of the
90
+ whole run, not 13.97% of `transform`.
91
+ - **Times** have three decimals, in `µs`, `ms` or `s`, whichever keeps the number below
92
+ 1000, so even very fast sections show a real value.
91
93
 
92
94
  Calling {func}`~execution_timer.clear_execution_timings` starts over with an empty
93
95
  registry.
@@ -37,12 +37,12 @@ print(get_execution_times_report(flatten=False))
37
37
  ```
38
38
 
39
39
  ```text
40
- Total time: 0.0724 s.
40
+ Total time: 67.003 ms.
41
41
 
42
- solve: 0.0724 s (100.00%)
43
- .. step[0]: 0.0125 s (17.31%)
44
- .. step[1]: 0.0250 s (34.60%)
45
- .. step[2]: 0.0347 s (47.97%)
42
+ solve: 67.003 ms (100.00%)
43
+ .. step[0]: 12.542 ms (18.72%)
44
+ .. step[1]: 23.851 ms (35.60%)
45
+ .. step[2]: 30.498 ms (45.52%)
46
46
  ```
47
47
 
48
48
  Use the separate view to find a slow iteration, and the merged view to see the loop's
@@ -32,16 +32,20 @@ print(get_execution_times_report())
32
32
  ```
33
33
 
34
34
  ```text
35
- Total time: 0.0499 s.
35
+ Total time: 48.159 ms.
36
36
 
37
- load_data: 0.0249 s (49.92%)
38
- solve: 0.0250 s (50.08%)
39
- .. step: 0.0249 s (49.97%)
37
+ load_data: 24.199 ms (50.25%)
38
+ solve: 23.960 ms (49.75%)
39
+ .. step: 23.904 ms (49.64%)
40
40
  ```
41
41
 
42
42
  Sections appear depth-first, each followed by its children, in the order they were first
43
43
  entered. Percentages are shares of the total time at every level.
44
44
 
45
+ Times have three decimals, in `µs`, `ms` or `s`, whichever keeps the number below 1000.
46
+ A 48 µs section reads `48.213 µs` rather than rounding to zero. The dictionary and JSON
47
+ readers below return plain seconds.
48
+
45
49
  ## Logging
46
50
 
47
51
  {func}`~execution_timer.log_execution_times` logs the same report at `INFO` level, starting
@@ -58,11 +62,11 @@ log_execution_times()
58
62
 
59
63
  ```text
60
64
  INFO:execution_timer._timer:
61
- Total time: 0.0499 s.
65
+ Total time: 48.159 ms.
62
66
 
63
- load_data: 0.0249 s (49.92%)
64
- solve: 0.0250 s (50.08%)
65
- .. step: 0.0249 s (49.97%)
67
+ load_data: 24.199 ms (50.25%)
68
+ solve: 23.960 ms (49.75%)
69
+ .. step: 23.904 ms (49.64%)
66
70
  ```
67
71
 
68
72
  It logs to the `execution_timer._timer` logger unless you pass your own with
@@ -152,11 +152,11 @@ print(get_execution_times_report())
152
152
  ```
153
153
 
154
154
  ```text
155
- Total time: 0.0354 s.
155
+ Total time: 34.704 ms.
156
156
 
157
- walk: 0.0354 s (100.00%)
158
- .. walk: 0.0247 s (69.77%)
159
- .. .. walk: 0.0125 s (35.38%)
157
+ walk: 34.704 ms (100.00%)
158
+ .. walk: 24.161 ms (69.62%)
159
+ .. .. walk: 11.603 ms (33.44%)
160
160
  ```
161
161
 
162
162
  For deep recursion, time the top-level call only, for example with a `with` block around
@@ -23,12 +23,12 @@ print(get_execution_times_report())
23
23
  ```
24
24
 
25
25
  ```text
26
- Total time: 0.3156 s.
26
+ Total time: 312.838 ms.
27
27
 
28
- load_data: 0.1219 s (38.62%)
29
- solve: 0.1937 s (61.38%)
30
- .. step: 0.1585 s (50.24%)
31
- .. postprocess: 0.0350 s (11.10%)
28
+ load_data: 124.682 ms (39.85%)
29
+ solve: 188.157 ms (60.15%)
30
+ .. step: 154.718 ms (49.46%)
31
+ .. postprocess: 33.295 ms (10.64%)
32
32
  ```
33
33
 
34
34
  ## Why executiontimer
@@ -18,7 +18,7 @@ from execution_timer._timer import (
18
18
  save_execution_timings_json,
19
19
  )
20
20
 
21
- __version__ = "1.0.3"
21
+ __version__ = "1.1.0"
22
22
 
23
23
  __all__ = [
24
24
  "DEFAULT_CATEGORY",
@@ -198,11 +198,11 @@ class _ExecutionTimer:
198
198
  # Total the unflattened paths, like get_total_time and the JSON export.
199
199
  total_time = _top_level_time(snapshot)
200
200
  timings = _flatten(snapshot) if flatten else snapshot
201
- report = [f"Total time: {total_time:.4f} s.\n"]
201
+ report = [f"Total time: {_format_duration(total_time)}.\n"]
202
202
  for key, depth in _ordered_by_hierarchy(timings):
203
203
  elapsed_time = timings[key]["elapsed_time"]
204
204
  percentage = (elapsed_time / total_time) * 100 if total_time else 0.0
205
- report.append(f"{'.. ' * depth}{key[-1]}: {elapsed_time:.4f} s ({percentage:.2f}%)")
205
+ report.append(f"{'.. ' * depth}{key[-1]}: {_format_duration(elapsed_time)} ({percentage:.2f}%)")
206
206
  return "\n".join(report)
207
207
 
208
208
  def compute_total_time(self) -> float:
@@ -243,6 +243,20 @@ class _ExecutionTimer:
243
243
  _TIMER: Final = _ExecutionTimer()
244
244
 
245
245
 
246
+ def _format_duration(seconds: float) -> str:
247
+ """Format seconds with three decimals in the largest of µs, ms and s that stays below 1000.
248
+
249
+ The unit is chosen after rounding, so 999.9996 µs reads ``1.000 ms`` rather than
250
+ ``1000.000 µs``. Sections of any length stay readable, where fixed seconds would show a
251
+ fast section as zero.
252
+ """
253
+ for scale, unit in ((1e6, "µs"), (1e3, "ms")):
254
+ scaled = seconds * scale
255
+ if round(scaled, 3) < 1000:
256
+ return f"{scaled:.3f} {unit}"
257
+ return f"{seconds:.3f} s"
258
+
259
+
246
260
  def _flatten(timings: dict[tuple[str, ...], _TimesDict]) -> dict[tuple[str, ...], _TimesDict]:
247
261
  flat_map: dict[tuple[str, ...], _TimesDict] = {}
248
262
  for key, info in timings.items():
@@ -378,8 +392,9 @@ def _basic_name_without_counter(name: str) -> str:
378
392
  def get_execution_times_report(*, flatten: bool = True) -> str:
379
393
  """Get a formatted, indented report of all recorded sections.
380
394
 
381
- Each line shows a section's accumulated seconds and its share of the total time.
382
- Indentation reflects nesting.
395
+ Each line shows a section's accumulated time and its share of the total time.
396
+ Indentation reflects nesting. Times have three decimals, in ``µs``, ``ms`` or ``s``,
397
+ whichever keeps the number below 1000.
383
398
 
384
399
  Args:
385
400
  flatten: Merge ``counter`` variants such as ``step[0]`` and ``step[1]`` into ``step``.
@@ -208,6 +208,25 @@ class TestReporting:
208
208
  assert get_execution_times_report() == ""
209
209
  assert caplog.records == []
210
210
 
211
+ @pytest.mark.parametrize(
212
+ ("seconds", "expected"),
213
+ [
214
+ (0.0000004, "0.400 µs"),
215
+ (0.0000482134, "48.213 µs"),
216
+ (0.0009999994, "999.999 µs"),
217
+ (0.0009999996, "1.000 ms"),
218
+ (0.3156, "315.600 ms"),
219
+ (0.9999996, "1.000 s"),
220
+ (2.5, "2.500 s"),
221
+ (3725.0, "3725.000 s"),
222
+ ],
223
+ )
224
+ def test_report_picks_a_unit_that_keeps_short_sections_visible(self, seconds: float, expected: str) -> None:
225
+ with patch.object(time, "perf_counter", side_effect=[0.0, seconds]), TimerContext("section"):
226
+ pass
227
+
228
+ assert get_execution_times_report() == f"Total time: {expected}.\n\nsection: {expected} (100.00%)"
229
+
211
230
  def test_report_flatten_flag(self) -> None:
212
231
  with patch.object(time, "perf_counter", side_effect=[0, 1, 1, 2]):
213
232
  for i in range(2):
@@ -757,7 +776,7 @@ class TestReportOrdering:
757
776
  pass
758
777
 
759
778
  report = get_execution_times_report()
760
- assert "instant: 0.0000 s (0.00%)" in report
779
+ assert "instant: 0.000 µs (0.00%)" in report
761
780
 
762
781
 
763
782
  class TestRobustness:
@@ -1059,7 +1078,7 @@ class TestLifecycle:
1059
1078
  assert list(raw_timings()) == [("parent", "child")]
1060
1079
  # The cleared parent is gone, so its child is top-level: it counts toward the total
1061
1080
  # and is not indented beneath an unrelated section.
1062
- assert get_execution_times_report() == "Total time: 2.0000 s.\n\nchild: 2.0000 s (100.00%)"
1081
+ assert get_execution_times_report() == "Total time: 2.000 s.\n\nchild: 2.000 s (100.00%)"
1063
1082
  assert get_total_time() == 2.0
1064
1083
  payload = cast(TimingsPayload, json.loads(get_execution_times_json()))
1065
1084
  assert payload["total_time"] == 2.0
@@ -1078,7 +1097,7 @@ class TestLifecycle:
1078
1097
  with TimerContext("after"):
1079
1098
  pass
1080
1099
  assert get_execution_times_report() == (
1081
- "Total time: 5.0000 s.\n\nload: 4.0000 s (80.00%)\n.. parse: 1.0000 s (20.00%)\nafter: 1.0000 s (20.00%)"
1100
+ "Total time: 5.000 s.\n\nload: 4.000 s (80.00%)\n.. parse: 1.000 s (20.00%)\nafter: 1.000 s (20.00%)"
1082
1101
  )
1083
1102
  assert get_total_time() == 5.0
1084
1103
 
File without changes