executiontimer 1.0.1__py3-none-any.whl → 1.0.2__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__ = "1.0.1"
21
+ __version__ = "1.0.2"
22
22
 
23
23
  __all__ = [
24
24
  "DEFAULT_CATEGORY",
execution_timer/_timer.py CHANGED
@@ -278,8 +278,9 @@ class TimerContext:
278
278
  """Decorate a function to time its execution under this context.
279
279
 
280
280
  Coroutine functions are wrapped so the timing spans the entire ``await``, not just
281
- creation of the coroutine object. Generator functions are rejected: a wrapper would
282
- time only creation of the generator object, not its iteration.
281
+ creation of the coroutine object. So is a coroutine returned by a plain function,
282
+ typically another decorator stacked on an ``async def``. Generator functions are
283
+ rejected: a wrapper would time only creation of the generator object, not its iteration.
283
284
  """
284
285
  if inspect.isgeneratorfunction(func) or inspect.isasyncgenfunction(func):
285
286
  msg = (
@@ -297,10 +298,20 @@ class TimerContext:
297
298
  @functools.wraps(func)
298
299
  def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
299
300
  with self:
300
- return func(*args, **kwargs)
301
+ result = func(*args, **kwargs)
302
+ if inspect.iscoroutine(result):
303
+ # A decorator between this one and an ``async def`` hides the coroutine function,
304
+ # so the call above only created the coroutine. Time awaiting it as well.
305
+ return cast("R", self._time_await(result))
306
+ return result
301
307
 
302
308
  return wrapper
303
309
 
310
+ async def _time_await(self, coroutine: Coroutine[object, object, T]) -> T:
311
+ """Await a coroutine that was created outside this context, timing the whole await."""
312
+ with self:
313
+ return await coroutine
314
+
304
315
  def _wrap_async(self, func: Callable[P, Coroutine[object, object, T]]) -> Callable[P, Coroutine[object, object, T]]:
305
316
  """Wrap a coroutine function so the timing spans the whole await."""
306
317
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: executiontimer
3
- Version: 1.0.1
3
+ Version: 1.0.2
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
@@ -134,6 +134,9 @@ Coroutine functions are supported natively — the timing spans the entire `awai
134
134
  async def fetch(url: str) -> bytes: ...
135
135
  ```
136
136
 
137
+ This also holds when another decorator sits between `TimerContext` and the `async def`
138
+ and returns its coroutine: the section covers both the call and the `await`.
139
+
137
140
  Generator functions (including `async` generators) cannot be decorated and raise a
138
141
  `TypeError`: the decorator would time only the creation of the generator object, not its
139
142
  iteration. Time the loop that consumes the generator with a `with` block instead.
@@ -153,7 +156,9 @@ get_execution_timings(flatten=False) # {("step[0]",): ..., ("step[1]",): ..., .
153
156
  ```
154
157
 
155
158
  Flattening removes the final integer suffix (including negative counters). Other
156
- bracketed names such as `array[index]` are preserved. If merged entries have different
159
+ bracketed names such as `array[index]` are preserved. Flattening cannot tell a counter
160
+ from a name you wrote yourself, so sections named `"row[1]"` and `"row[2]"` are also merged
161
+ into `row`; use `flatten=False` to keep them apart. If merged entries have different
157
162
  categories, the category from the most recently entered section is used.
158
163
 
159
164
  Each counter value is stored as its own section until `clear_execution_timings()` is
@@ -286,7 +291,6 @@ an active section's current duration is added only when it exits.
286
291
  Run the repeatable benchmark with `uv run python benchmarks/overhead.py`. It measures
287
292
  fresh and reused contexts, sync and async decorators, nesting, and reporting. Compare
288
293
  results using the same interpreter and machine; see [benchmarks/README.md](https://github.com/seba2390/ExecutionTimer/blob/main/benchmarks/README.md).
289
- `log_execution_times()` skips building a report when its logger has `INFO` disabled.
290
294
 
291
295
  ## API
292
296
 
@@ -294,7 +298,7 @@ results using the same interpreter and machine; see [benchmarks/README.md](https
294
298
  | --- | --- |
295
299
  | `TimerContext(name, category=DEFAULT_CATEGORY, counter=None)` | Context manager **and** decorator for timing a section. |
296
300
  | `get_execution_times_report(*, flatten=True)` | Formatted, indented report of all sections (`""` if none). |
297
- | `log_execution_times(*, flatten=True, logger=None)` | Log that report at `INFO` level (a warning if empty). |
301
+ | `log_execution_times(*, flatten=True, logger=None)` | Log that report at `INFO` level (a warning if empty); a no-op if `INFO` is disabled. |
298
302
  | `get_execution_timings(*, flatten=True)` | Timings as `dict[tuple[str, ...], TimingReport]`. |
299
303
  | `get_execution_times_json(*, flatten=True, indent=2)` | All timings as a JSON string. |
300
304
  | `save_execution_timings_json(path, *, flatten=True, indent=2)` | Write timings to a JSON file; returns the `Path`. |
@@ -0,0 +1,7 @@
1
+ execution_timer/__init__.py,sha256=WBDnG2RxhKZmuhb5U4wgVV4WNgvZehegtfBuSh7AA6I,959
2
+ execution_timer/_timer.py,sha256=ermOhB2xWM4acsGgkHuN3MyeA9uqpa9FgNuoAABdGjY,17439
3
+ execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ executiontimer-1.0.2.dist-info/METADATA,sha256=7hk9S89FJ0b9H-O2IAWak-c_ef55RS1Vq1pJPh4gCGY,13900
5
+ executiontimer-1.0.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ executiontimer-1.0.2.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
7
+ executiontimer-1.0.2.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- execution_timer/__init__.py,sha256=SDCT34iz3KKhr1CyiCpvMrjjdf1umy55sScSp4bKUBo,959
2
- execution_timer/_timer.py,sha256=4RP4SDki9TasaCfk56lmJX54EiYnQsvHOd5WnZuuoxI,16778
3
- execution_timer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- executiontimer-1.0.1.dist-info/METADATA,sha256=o5McC2gZg2rh2G65qv0Hh6tdmGo6Mk_TfPfym_OmKhA,13612
5
- executiontimer-1.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
- executiontimer-1.0.1.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
7
- executiontimer-1.0.1.dist-info/RECORD,,