Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# API Reference

This page summarizes the callable surface of **fspin**, including decorator usage, the context manager, and the underlying `RateControl` class.

## Unified `spin`

The top-level `spin` exported by `fspin` is a unified entry point that behaves as either a decorator or a context manager depending on how it is invoked:

- `@spin(freq=10)` wraps a function or coroutine and runs it at the given frequency when called.
- `with spin(func, freq=10):` or `async with spin(func, freq=10):` starts looping immediately upon entering the context and stops on exit.

### Decorator signature

```python
from fspin import spin

@spin(freq, condition_fn=None, report=False, thread=False, wait=False)
def worker(...):
...
```

Parameters:

- **freq (float)**: Target frequency in Hz.
- **condition_fn (callable or coroutine, optional)**: Predicate evaluated before each iteration. Sync workers must provide a regular callable; async workers may supply a callable or coroutine and awaitable results are handled automatically. Defaults to always continue.
- **report (bool)**: Enable performance reporting.
- **thread (bool)**: For synchronous functions, run in a separate thread instead of blocking the caller.
- **wait (bool)**:
- Async: whether to await the created task (blocking) or return immediately (fire-and-forget).
- Sync threaded: whether to join the background thread before returning.

Returns the managing `RateControl` instance.

### Context manager signature

```python
from fspin import spin

with spin(func, freq, *func_args, condition_fn=None, report=False, thread=True, wait=False, **func_kwargs) as rc:
...

# or async with for coroutines
```

Arguments mirror the decorator, but positional and keyword arguments after `freq` are forwarded to the worker each iteration. For synchronous workers, `wait=True` blocks entering the context body until the loop completes; asynchronous contexts ignore `wait` and stop on exit.

A deprecated alias `loop` provides the same behaviour with a warning.

## `RateControl`

```python
from fspin import rate
rc = rate(freq, is_coroutine, report=False, thread=True)
```

Constructor arguments:

- **freq (float)**: Desired frequency in Hz (must be > 0).
- **is_coroutine (bool)**: Whether the target callable is async.
- **report (bool)**: Collect metrics for reporting.
- **thread (bool)**: For synchronous functions, run in a background thread instead of blocking.

Key attributes available after creation and during execution:

- **loop_duration**: Desired loop duration in seconds.
- **report**: Whether reporting is enabled.
- **thread**: Whether threading is used for synchronous functions.
- **exceptions**: List of exceptions seen while spinning.
- **status**: Current status (`"running"` or `"stopped"`).
- **mode**: Execution mode (`"async"`, `"sync-threaded"`, or `"sync-blocking"`).
- **frequency**: Current target frequency in Hz; settable to adjust runtime rate.
- **elapsed_time**: Seconds since the loop started.
- **exception_count**: Number of exceptions encountered.

### Starting and stopping loops

- **start_spinning(func, condition_fn, *args, **kwargs)**: Dispatches to async or sync execution depending on `is_coroutine`. Returns an `asyncio.Task`, `threading.Thread`, or `None` depending on mode.
- **start_spinning_sync(func, condition_fn, *args, wait=False, **kwargs)**: Run synchronously, optionally on a background thread when `thread=True`; joins the thread if `wait=True`.
- **start_spinning_async(func, condition_fn, *args, **kwargs)**: Create an asyncio task that awaits the worker each iteration.
- **start_spinning_async_wrapper(func, condition_fn=None, wait=False, **kwargs)**: Convenience wrapper to await completion (`wait=True`) or return immediately (`wait=False`).
- **stop_spinning()**: Signal the loop to stop and join or cancel underlying execution where appropriate.

### Reporting and status

- **get_report(output=True)**: Aggregate performance data (frequency, loop durations, deviations, iterations, and exceptions) and print a formatted report when `output=True`. Returns a dictionary of metrics.
- **is_running()**: Boolean indicating if the loop is still active.
- **__str__() / __repr__()**: Human- and developer-friendly representations summarizing mode, frequency, durations, and counters.

## Reporting helpers

`ReportLogger` handles terminal-friendly output and histogram generation for deviation data when reporting is enabled. Users typically access it indirectly via `RateControl.get_report()`, but it is available at `fspin.reporting.ReportLogger` for advanced customization.
52 changes: 52 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# fspin

**fspin** provides ROS-like rate control for Python functions and coroutines so they execute at a consistent frequency. The library automatically detects whether a callable is synchronous or asynchronous and runs it with optional threading support, run-time frequency adjustments, and performance reporting.

## Features

- Spin functions at a target frequency with optional performance reports and deviation compensation.
- Use the unified `spin` entry point as a decorator or as a context manager for both sync and async callables.
- Adjust loop frequency during execution while inspecting current status and elapsed time.
- Support both blocking usage and fire-and-forget patterns for threaded or asynchronous workloads.

## Quickstart

Create a loop that runs a synchronous function in a background thread at 1 kHz and prints a report:

```python
import time
from fspin import spin

@spin(freq=1000, report=True)
def function_to_loop():
# things to loop
time.sleep(0.0005) # a fake task to take 0.5ms

# call the function
function_to_loop() # this will be blocking, and start looping
# it'll automatically catch the keyboard interrupt
```

For an asynchronous worker, you can block or return immediately:

```python
import asyncio
from fspin import spin

# Blocking version (wait=True)
@spin(freq=2, report=True, wait=True)
async def blocking_loop():
await asyncio.sleep(0.1)

# Fire-and-forget version (wait=False)
@spin(freq=2, report=True, wait=False)
async def non_blocking_loop():
await asyncio.sleep(0.1)

async def run_both():
rc1 = await blocking_loop() # awaits completion before returning
rc2 = await non_blocking_loop() # returns immediately; remember to stop later
rc2.stop_spinning()
```

See the [Usage Examples](usage.md) for more scenarios and the [API Reference](api.md) for complete argument details.
31 changes: 31 additions & 0 deletions docs/llm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# LLM Guide

This page is written for LLM agents that need to generate or reason about code using **fspin**. It highlights the core entry points, argument behaviors, and safety checks so responses stay accurate to the library.

## Core interfaces

- **Unified `spin`**: Acts as both decorator and context manager. When called with no positional arguments or with a numeric first argument it returns a decorator; when the first argument is callable it creates a context manager that immediately starts spinning on entry and stops on exit. The managing `RateControl` is returned in all cases.
- **`RateControl`**: Lower-level controller created via `fspin.rate(freq, is_coroutine, report=False, thread=True)`. Tracks state (`status`, `mode`, `frequency`, `elapsed_time`, `exception_count`) and exposes `start_spinning`, `stop_spinning`, and `get_report` helpers.

## Argument semantics to remember

- `freq` is required and must be greater than zero; a `ValueError` is raised otherwise.
- `thread` only affects synchronous functions; async workers ignore it.
- `wait` is interpreted differently by mode:
- **Async decorators**: `wait=True` awaits the loop to finish before returning; `wait=False` returns immediately (fire-and-forget) and leaves stopping to the caller via `stop_spinning()`.
- **Sync threaded decorators/contexts**: `wait=True` joins the background thread before returning; `wait=False` leaves the loop running in the background while control returns.
- **Async contexts**: `wait` is ignored; the loop runs during the `async with` block and stops on exit.
- `condition_fn` must be a regular callable for synchronous spinning; awaitable predicates are rejected. Async spinning accepts coroutine or awaitable predicates and awaits them before each iteration.
- `report=True` enables collection of loop durations, deviations, and counts that can be fetched via `get_report(output=False)` for programmatic inspection.

## Rate limits and warnings

- Async spinning issues platform-specific warnings if the requested frequency is unlikely to be achievable (roughly >65Hz on Windows, >925Hz on Linux, >4000Hz on macOS). Suggest synchronous mode for higher rates when these warnings appear.

## Prompting tips for LLMs

- Prefer the decorator for user-facing examples and reach for the context manager when users need scoped lifetimes.
- When suggesting fire-and-forget async loops, remind users to hold onto the returned `RateControl` and call `stop_spinning()` when done.
- Mention that synchronous `condition_fn` must not be async, and recommend wrapping async checks in a synchronous adapter if needed.
- Include `report=True` in examples when users want metrics, and explain that reports are printed unless `output=False` is passed to `get_report()`.
- If users mention platform-specific timing issues or high target frequencies, surface the built-in warnings and propose lowering `freq` or switching to synchronous threading.
115 changes: 115 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Usage Examples

These examples mirror the patterns provided in the repository and demonstrate synchronous and asynchronous spinning, blocking versus fire-and-forget behaviour, and the context manager interface.

## Sync threaded: blocking vs fire-and-forget

```python
import time
from fspin import spin

counter = {"n": 0}

def cond():
return counter["n"] < 5

# Fire-and-forget: returns immediately while the background thread runs
@spin(freq=50, condition_fn=cond, thread=True, wait=False)
def sync_bg():
counter["n"] += 1

rc = sync_bg() # returns immediately
# ... do other work ...
rc.stop_spinning() # stop when ready

# Blocking: call does not return until cond() becomes False
@spin(freq=50, condition_fn=cond, thread=True, wait=True)
def sync_blocking():
counter["n"] += 1

counter["n"] = 0
rc2 = sync_blocking() # blocks until 5 iterations complete
```

## Async decorator: blocking vs fire-and-forget

```python
import asyncio
from fspin import spin

# Blocking version (wait=True)
@spin(freq=2, report=True, wait=True)
async def blocking_loop():
await asyncio.sleep(0.1)

# Fire-and-forget version (wait=False)
@spin(freq=2, report=True, wait=False)
async def non_blocking_loop():
await asyncio.sleep(0.1)

async def run_both():
rc1 = await blocking_loop() # awaits completion before returning
rc2 = await non_blocking_loop() # returns immediately; remember to stop later
rc2.stop_spinning()
```

## Context manager for synchronous functions

```python
import time
from fspin import spin

def heartbeat():
print(f"Heartbeat at {time.strftime('%H:%M:%S')}")

# Runs in background thread at 2Hz, auto-stops on exit, prints report
with spin(heartbeat, freq=2, report=True, thread=True):
time.sleep(5) # keep the block alive for 5s
print("exiting the loop")
print("Loop exited")
```

## Context manager with async predicate support

```python
import asyncio
from fspin import spin

ticks = []

async def predicate():
await asyncio.sleep(0) # simulate async state checks
return len(ticks) < 3

@spin(freq=100, condition_fn=predicate, wait=True)
async def monitored_task():
ticks.append("tick")

async def main():
rc = await monitored_task()
assert len(ticks) == 2
assert rc.status == "stopped"

asyncio.run(main())
```

## Manual control with `rate`

```python
import time
from fspin import rate

# Create a rate control for a simple function
rc = rate(freq=10, is_coroutine=False, report=True, thread=True)

# Start spinning your function in background
rc.start_spinning(lambda: print("Tick"), None)

# Let it run 3 seconds
time.sleep(3)

# Stop the loop and print report
rc.stop_spinning()
```

For more runnable scripts, see the files under `example/` in the repository.
24 changes: 24 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
site_name: fspin Documentation
site_description: Fixed-rate looping utilities for Python functions and coroutines
repo_url: https://github.com/Suke0811/fspin
theme:
name: material
palette:
- scheme: slate
primary: black
accent: amber
features:
- navigation.sections
- navigation.instant
- content.code.copy
markdown_extensions:
- admonition
- codehilite
- def_list
- toc:
permalink: true
nav:
- Home: index.md
- Usage Examples: usage.md
- API Reference: api.md
- LLM Guide: llm.md
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pytest>=8.2
pytest-asyncio>=1.2
pytest-timeout>=2.4

mkdocs-material>=9.5