Metadata-Version: 2.4
Name: howlong-pyalp
Version: 0.1.0
Summary: A minimal, zero-dependency Python decorator for measuring how long your functions take to run.
Author-email: Alperen <byalperen.dev@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Alperen
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/BYALPERENK/howlong-pyalp
Project-URL: Repository, https://github.com/BYALPERENK/howlong-pyalp
Project-URL: Issues, https://github.com/BYALPERENK/howlong-pyalp/issues
Project-URL: Changelog, https://github.com/BYALPERENK/howlong-pyalp/blob/main/README.md#changelog
Keywords: timer,timing,decorator,benchmark,performance,profiling,runtime,elapsed
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Benchmark
Classifier: Typing :: Typed
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# howlong-pyalp

[![PyPI version](https://img.shields.io/pypi/v/howlong-pyalp.svg)](https://pypi.org/project/howlong-pyalp/)
[![Python versions](https://img.shields.io/pypi/pyversions/howlong-pyalp.svg)](https://pypi.org/project/howlong-pyalp/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Downloads](https://static.pepy.tech/badge/howlong-pyalp)](https://pepy.tech/project/howlong-pyalp)

A minimal, zero-dependency Python decorator for measuring how long your functions take to run.

```python
from howlong_pyalp import howlong

@howlong
def process_data(n):
    return sum(i ** 2 for i in range(n))

process_data(1_000_000)
# [process_data] 142.871 milliseconds
```

---

## Table of Contents

- [Why howlong?](#why-howlong)
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Usage](#usage)
  - [Three calling forms](#three-calling-forms)
  - [Choosing a time unit](#choosing-a-time-unit)
  - [Choosing a measurement mode](#choosing-a-measurement-mode)
- [API Reference](#api-reference)
- [How It Works](#how-it-works)
- [Examples](#examples)
- [Caveats & Limitations](#caveats--limitations)
- [Requirements](#requirements)
- [Contributing](#contributing)
- [Changelog](#changelog)
- [License](#license)

---

## Why howlong?

Profiling tools like `cProfile` are powerful but heavy-handed when all you want to know is *"how long did this function take?"*. The `timeit` module is excellent for micro-benchmarks but verbose for ad-hoc timing. Manually calling `time.perf_counter()` before and after a function is repetitive and easy to get wrong.

`howlong` solves this with a single decorator:

- Apply `@howlong` above any function — that's it.
- Get readable, formatted output on every call.
- Choose between wall-clock-like and CPU-only measurement.
- Pick the time unit, or let it choose automatically.

No setup, no configuration files, no dependencies.

---

## Features

- **Single-line integration.** Drop `@howlong` on any function.
- **Three calling forms.** `@howlong`, `@howlong()`, and `@howlong(unit="ms")` all work.
- **Two measurement modes.** Wall-clock-like elapsed time (`perf`) or pure CPU time (`cpu`).
- **Flexible units.** Seconds, milliseconds, microseconds, nanoseconds — or `"auto"` for the most readable one.
- **Preserves function metadata.** Name, docstring, type hints, and signature are kept intact via `functools.wraps`.
- **Exception-safe.** Timing is always reported, even if the wrapped function raises.
- **Zero dependencies.** Pure standard library — no compatibility headaches.
- **Tiny footprint.** A single file, well under 100 lines of code.

---

## Installation

Install from PyPI using `pip`:

```bash
pip install howlong-pyalp
```

To install from source:

```bash
git clone https://github.com/BYALPERENK/howlong-pyalp.git
cd howlong-pyalp
pip install .
```

> **Note:** The package is named `howlong-pyalp` on PyPI (with a hyphen) but imported as `howlong_pyalp` (with an underscore). This is a Python naming convention — hyphens are not valid in module names.

---

## Quick Start

```python
from howlong_pyalp import howlong

@howlong
def my_function():
    total = 0
    for i in range(100_000):
        total += i * i
    return total

my_function()
# [my_function] 6.734 milliseconds
```

That's it. The function still works exactly as before, but now prints how long each call took.

---

## Usage

### Three calling forms

The decorator can be applied in three equivalent ways:

```python
from howlong_pyalp import howlong

# 1. Bare — no parentheses, default settings
@howlong
def f(): ...

# 2. Parenthesized — same as bare, but explicit
@howlong()
def f(): ...

# 3. Configured — with one or more parameters
@howlong(unit="us")
def f(): ...

@howlong(mode="cpu", unit="ms")
def f(): ...
```

All three return a decorated function whose runtime is printed on every call. The bare form is convenient; the parenthesized form is useful when you want a consistent style across your codebase.

### Choosing a time unit

The `unit` parameter controls how elapsed time is formatted in the output.

| Value      | Output example                        |
|------------|---------------------------------------|
| `"s"`      | `[f] 1.245 seconds`                   |
| `"ms"`     | `[f] 1245.388 milliseconds` (default) |
| `"us"`     | `[f] 1245388.000 microseconds`        |
| `"ns"`     | `[f] 1245388012 nanoseconds`          |
| `"auto"`   | Picks the most readable unit based on the actual elapsed time |

The default unit is `"ms"`, which is appropriate for most functions. For short snippets or micro-benchmarks, switch to `"us"` or `"ns"`. For long-running tasks, `"s"` reads more naturally. When the runtime is unpredictable, `"auto"` adapts automatically:

```python
@howlong(unit="auto")
def adaptive(n):
    return sum(range(n))

adaptive(100)
# [adaptive] 12.413 microseconds

adaptive(10_000_000)
# [adaptive] 2.108 seconds
```

### Choosing a measurement mode

The `mode` parameter selects which underlying clock is used.

| Value     | Underlying clock      | What it measures                                          |
|-----------|-----------------------|-----------------------------------------------------------|
| `"perf"`  | `time.perf_counter`   | Wall-clock-like elapsed time, including sleeps and I/O    |
| `"cpu"`   | `time.process_time`   | CPU time only — sleeps and I/O are excluded               |

The default is `"perf"`, which answers the question *"How long did the user wait?"*.

Use `"cpu"` when you want to measure how hard the CPU actually worked, ignoring time spent waiting for the disk, the network, or `time.sleep()`. This is useful for comparing the computational cost of algorithms regardless of system load or I/O variance.

```python
import time
from howlong_pyalp import howlong

@howlong(mode="perf")
def with_sleep():
    time.sleep(0.5)
    return sum(i * i for i in range(100_000))

@howlong(mode="cpu")
def with_sleep_cpu():
    time.sleep(0.5)
    return sum(i * i for i in range(100_000))

with_sleep()
# [with_sleep] 505.088 milliseconds

with_sleep_cpu()
# [with_sleep_cpu] 4.913 milliseconds
```

Notice the dramatic difference: `perf` includes the 500ms sleep, while `cpu` reports only the time the CPU actually spent computing.

---

## API Reference

### `howlong(func=None, *, unit="ms", mode="perf")`

Decorator that measures and prints the runtime of the wrapped function.

**Parameters**

- **`func`** *(callable, optional)*
  The function to decorate. Filled in automatically when using `@howlong` without parentheses. You should not pass this explicitly.

- **`unit`** *(str, default `"ms"`)*
  Time unit for the printed output. One of `"auto"`, `"s"`, `"ms"`, `"us"`, `"ns"`.
  Raises `ValueError` for any other value.

- **`mode`** *(str, default `"perf"`)*
  Which clock to use. One of `"perf"` or `"cpu"`.
  Raises `ValueError` for any other value.

**Returns**

The decorated function. The wrapped function behaves identically to the original — same arguments, same return value, same exceptions — but its runtime is printed to standard output on every call.

**Output format**

```
[function_name] <value> <unit_name>
```

For example: `[process_data] 142.871 milliseconds`

---

## How It Works

The decorator wraps your function in a small piece of code that:

1. Records the current time using the selected clock.
2. Calls your function inside a `try` block.
3. In the `finally` block, computes the elapsed time and prints it.

The `try/finally` ensures the timing is printed even if the function raises. Exceptions are not swallowed — they propagate normally.

Two clocks are available:

- **`time.perf_counter`** is Python's highest-resolution monotonic clock. It is not affected by system clock adjustments (such as NTP corrections) and can have nanosecond resolution depending on the platform.

- **`time.process_time`** measures CPU time consumed by the current process. It excludes time spent sleeping or waiting on I/O.

Function metadata (`__name__`, `__doc__`, `__annotations__`, `__qualname__`, etc.) is preserved through `functools.wraps`. The original undecorated function remains accessible via the `__wrapped__` attribute, which makes the decorator compatible with introspection tools, test frameworks, and documentation generators such as Sphinx.

---

## Examples

### Timing a numerical computation

```python
from howlong_pyalp import howlong

@howlong
def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

fibonacci(100_000)
# [fibonacci] 89.421 milliseconds
```

### Comparing wall time and CPU time for I/O-bound code

```python
import requests
from howlong_pyalp import howlong

@howlong(mode="perf")
def fetch_perf():
    return requests.get("https://example.com").status_code

@howlong(mode="cpu")
def fetch_cpu():
    return requests.get("https://example.com").status_code

fetch_perf()
# [fetch_perf] 312.482 milliseconds  <- dominated by network wait

fetch_cpu()
# [fetch_cpu] 8.221 milliseconds     <- only the actual CPU work
```

### Using `"auto"` for variable workloads

```python
@howlong(unit="auto")
def variable_workload(size):
    return sum(i * i for i in range(size))

variable_workload(100)
# [variable_workload] 4.812 microseconds

variable_workload(10_000_000)
# [variable_workload] 1.024 seconds
```

### Metadata is preserved

```python
@howlong
def documented(x: int) -> int:
    """Return the square of x."""
    return x * x

print(documented.__name__)         # documented
print(documented.__doc__)          # Return the square of x.
print(documented.__annotations__)  # {'x': int, 'return': int}
```

### Exceptions still propagate

```python
@howlong
def will_fail():
    raise ValueError("oops")

will_fail()
# [will_fail] 0.012 milliseconds
# Traceback (most recent call last):
#   ...
# ValueError: oops
```

The timing is still printed, and the exception is re-raised normally.

### Stacking with other decorators

```python
from functools import lru_cache
from howlong_pyalp import howlong

@howlong
@lru_cache(maxsize=128)
def expensive(n):
    return sum(i ** 2 for i in range(n))

expensive(1_000_000)  # First call: actual computation
# [expensive] 142.871 milliseconds

expensive(1_000_000)  # Second call: served from cache
# [expensive] 0.003 milliseconds
```

Decorator order matters. Placing `@howlong` on the outside (closer to the `def`) means it measures the time after caching is applied — useful for seeing cache effectiveness.

---

## Caveats & Limitations

- **Single-call measurements are noisy at the microsecond level.** A single timed call of a tiny function is unreliable due to OS scheduling, garbage collection, and CPU frequency scaling. For micro-benchmarks, use Python's built-in `timeit` module, which runs the code many times and reports statistics.

- **No support for `async` functions.** Wrapping a coroutine function with this decorator will time how long it takes to *create* the coroutine, not how long the awaited operation takes. Async support is on the roadmap.

- **Generators time only the call, not the iteration.** A generator function returns a generator object instantly. The decorator reports the time to create that object, not the time spent consuming it.

- **Output goes to `stdout` via `print`.** This is intentional for simplicity but means you cannot easily redirect or silence the output. A future version may switch to `logging` for greater flexibility.

- **Not thread-safe in the sense of grouped output.** If multiple threads call decorated functions concurrently, their `print` statements may interleave on the same line. The timing measurements themselves remain accurate per call.

- **`mode="cpu"` resolution is lower than `mode="perf"`.** Depending on the platform, `process_time` may have a coarser resolution (often around 10-15 milliseconds on Windows). For sub-millisecond CPU measurements, prefer `perf` mode.

---

## Requirements

- Python 3.7 or newer.
- No external dependencies.

Tested on Linux, macOS, and Windows.

---

## Contributing

Contributions are welcome. If you find a bug, have a feature request, or want to submit a pull request:

1. Open an issue on [GitHub](https://github.com/BYALPERENK/howlong-pyalp/issues) describing the problem or proposal.
2. Fork the repository and create a feature branch.
3. Add tests for any new behavior.
4. Run the test suite locally:
   ```bash
   pip install -e ".[dev]"
   pytest
   ```
5. Submit a pull request with a clear description of the change.

Please keep contributions consistent with the existing code style and the zero-dependency principle.

---

## Changelog

### 0.1.0
- Initial release.
- `howlong` decorator with `unit` and `mode` parameters.
- Support for bare, parenthesized, and configured calling forms.
- Automatic unit selection via `unit="auto"`.

---

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full text.
