executiontimer 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
@@ -18,7 +18,7 @@ from execution_timer._timer import (
18
18
  save_execution_timings_json,
19
19
  )
20
20
 
21
- __version__ = "0.1.1"
21
+ __version__ = "0.2.0"
22
22
 
23
23
  __all__ = [
24
24
  "DEFAULT_CATEGORY",
execution_timer/_timer.py CHANGED
@@ -247,8 +247,16 @@ class TimerContext:
247
247
  """Decorate a function to time its execution under this context.
248
248
 
249
249
  Coroutine functions are wrapped so the timing spans the entire ``await``, not just
250
- creation of the coroutine object.
250
+ creation of the coroutine object. Generator functions are rejected: a wrapper would
251
+ time only creation of the generator object, not its iteration.
251
252
  """
253
+ if inspect.isgeneratorfunction(func) or inspect.isasyncgenfunction(func):
254
+ msg = (
255
+ f"Cannot decorate generator function {func.__qualname__!r}: only creating the generator "
256
+ "would be timed. Time the loop that consumes it, or its body between yields, with a "
257
+ "'with TimerContext(...)' block instead."
258
+ )
259
+ raise TypeError(msg)
252
260
  if inspect.iscoroutinefunction(func):
253
261
  # ``iscoroutinefunction`` narrows nothing useful for the type checker, so bridge
254
262
  # through an explicitly typed helper instead of leaking ``Any`` into the signature.
@@ -299,7 +307,10 @@ def log_execution_times(*, flatten: bool = True, logger: logging.Logger | None =
299
307
  """Log the execution-times report at INFO level; flatten counters if requested."""
300
308
  target = logger if logger is not None else _LOGGER
301
309
  if target.isEnabledFor(logging.INFO):
302
- target.info(get_execution_times_report(flatten=flatten))
310
+ report = get_execution_times_report(flatten=flatten)
311
+ # An empty report has already been warned about; logging it would add a blank record.
312
+ if report:
313
+ target.info(report)
303
314
 
304
315
 
305
316
  def get_execution_timings(*, flatten: bool = True) -> dict[tuple[str, ...], TimingReport]:
@@ -345,7 +356,11 @@ def save_execution_timings_json(path: str | Path, *, flatten: bool = True, inden
345
356
 
346
357
 
347
358
  def get_total_time(*, flatten: bool = True) -> float:
348
- """Get total elapsed seconds across all top-level sections."""
359
+ """Get total elapsed seconds across all top-level sections.
360
+
361
+ ``flatten`` has no effect, because merging counter variants cannot change the total. It is
362
+ accepted for symmetry with the other reporting functions.
363
+ """
349
364
  return _TIMER.compute_total_time(flatten=flatten)
350
365
 
351
366
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: executiontimer
3
- Version: 0.1.1
3
+ Version: 0.2.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://github.com/seba2390/ExecutionTimer#readme
@@ -16,15 +16,17 @@ Classifier: Intended Audience :: Developers
16
16
  Classifier: Intended Audience :: Science/Research
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
19
20
  Classifier: Programming Language :: Python :: 3.12
20
21
  Classifier: Programming Language :: Python :: 3.13
21
22
  Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
22
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
23
25
  Classifier: Topic :: Software Development
24
26
  Classifier: Topic :: Software Development :: Testing
25
27
  Classifier: Topic :: System :: Benchmark
26
28
  Classifier: Typing :: Typed
27
- Requires-Python: >=3.12
29
+ Requires-Python: >=3.11
28
30
  Description-Content-Type: text/markdown
29
31
 
30
32
  <p align="center">
@@ -55,7 +57,8 @@ Zero dependencies. Fully type annotated. Works with threads and `asyncio`.
55
57
  - 🌳 **Automatic hierarchy** — nesting `with` blocks nests the report, no wiring required
56
58
  - 🏷️ **User-defined categories** — tag sections with any string (`"gpu"`, `"io"`, `"db"`) and get per-category totals
57
59
  - ⚡ **Native async** — decorating an `async def` times the whole `await`, not the coroutine object
58
- - 🧵 **Thread and task safe** — context stacks are isolated per thread and per asyncio task
60
+ - 🧵 **Thread and task safe** — context stacks are isolated per thread and per asyncio task,
61
+ including on free-threaded Python builds
59
62
  - 🔢 **Loop counters** — time each iteration separately, then merge them back together
60
63
  - 📤 **JSON export** — structured output for dashboards, CI, or an LLM
61
64
  - 🚫 **Nesting rules** — optionally forbid one category inside another to catch mistakes early
@@ -71,7 +74,7 @@ pip install executiontimer
71
74
  uv add executiontimer
72
75
  ```
73
76
 
74
- Requires Python 3.12+.
77
+ Requires Python 3.11+.
75
78
 
76
79
  > **Note** — the install name is `executiontimer`, the import name is `execution_timer`:
77
80
  >
@@ -131,6 +134,10 @@ Coroutine functions are supported natively — the timing spans the entire `awai
131
134
  async def fetch(url: str) -> bytes: ...
132
135
  ```
133
136
 
137
+ Generator functions (including `async` generators) cannot be decorated and raise a
138
+ `TypeError`: the decorator would time only the creation of the generator object, not its
139
+ iteration. Time the loop that consumes the generator with a `with` block instead.
140
+
134
141
  ### Counters
135
142
 
136
143
  Pass `counter=i` to time loop iterations separately. The report merges them by default
@@ -233,6 +240,18 @@ New asyncio tasks inherit the timing context in which they are created. Their se
233
240
  nest under that parent; changes to each task's active stack remain independent. Await
234
241
  child tasks inside the parent section if you want the parent duration to include them.
235
242
 
243
+ New threads do *not* inherit the timing context, so sections recorded in a thread
244
+ appear at the top level of the report, and their time is added to the total alongside
245
+ the section that started the thread. To nest thread work under the current section, run
246
+ it with `contextvars.copy_context().run(...)` or `asyncio.to_thread(...)`. Either way,
247
+ concurrent threads accumulate overlapping time. (Free-threaded builds of Python 3.14 make
248
+ threads inherit the context by default.)
249
+
250
+ Generators run in their caller's context. A `with TimerContext(...)` block that stays open
251
+ across a `yield` therefore also contains whatever the caller times while the generator is
252
+ paused, and its duration includes that paused time. Close sections before yielding, or
253
+ time the loop that consumes the generator instead.
254
+
236
255
  ### Reusing contexts and clearing timings
237
256
 
238
257
  A `TimerContext` can be reused, nested within itself, or shared by concurrent calls.
@@ -267,7 +286,7 @@ results using the same interpreter and machine; see [benchmarks/README.md](bench
267
286
  | `get_execution_timings(*, flatten=True)` | Timings as `dict[tuple[str, ...], TimingReport]`. |
268
287
  | `get_execution_times_json(*, flatten=True, indent=2)` | All timings as a JSON string. |
269
288
  | `save_execution_timings_json(path, *, flatten=True, indent=2)` | Write timings to a JSON file; returns the `Path`. |
270
- | `get_total_time(*, flatten=True)` | Total seconds across all top-level sections. |
289
+ | `get_total_time(*, flatten=True)` | Total seconds across all top-level sections (`flatten` has no effect on the sum). |
271
290
  | `get_total_category_time(category)` | Total seconds in a category (top-most entries only). |
272
291
  | `clear_execution_timings()` | Reset all recorded timings. |
273
292
  | `register_forbidden_nesting(outer, inner)` | Forbid `inner` category directly inside `outer`. |
@@ -0,0 +1,7 @@
1
+ execution_timer/__init__.py,sha256=9A5FYIrOvXbohuQMyIis651hYdNmzvNLQxfR02pk92w,959
2
+ execution_timer/_timer.py,sha256=4PSyP501arREnqOKtXEMwjbsb5sFISVYFRc1EXOXGP0,15366
3
+ execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ executiontimer-0.2.0.dist-info/METADATA,sha256=B6G1utu8t7BPwVh8KFID_pPr-c9z2bW0M6hd8-eiPWg,12631
5
+ executiontimer-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ executiontimer-0.2.0.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
7
+ executiontimer-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.3
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,7 +0,0 @@
1
- execution_timer/__init__.py,sha256=WC6Cn6-mGTEHh1386OJ--hrAFaa9Z6eSBFze21brEfw,959
2
- execution_timer/_timer.py,sha256=RbLD7HshcDBlcz0II7JiLZduuNr6Dao0D_d0gs9cuww,14526
3
- execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- executiontimer-0.1.1.dist-info/METADATA,sha256=sAJT1E9gFJHlfd2oVwAwcrWYebEhq3xhgmjMIvv3NS4,11374
5
- executiontimer-0.1.1.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
6
- executiontimer-0.1.1.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
7
- executiontimer-0.1.1.dist-info/RECORD,,