From cb11b5e99c01790e2e2b2d561ea61a977749d78e Mon Sep 17 00:00:00 2001 From: Suke0811 <49264928+Suke0811@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:37:23 -0800 Subject: [PATCH] Add LLM guide to documentation --- docs/api.md | 91 ++++++++++++++++++++++++++++++++++ docs/index.md | 52 +++++++++++++++++++ docs/llm.md | 31 ++++++++++++ docs/usage.md | 115 +++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 24 +++++++++ requirements-dev.txt | 2 + 6 files changed, 315 insertions(+) create mode 100644 docs/api.md create mode 100644 docs/index.md create mode 100644 docs/llm.md create mode 100644 docs/usage.md create mode 100644 mkdocs.yml diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..caa114a --- /dev/null +++ b/docs/api.md @@ -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. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..caf8cd1 --- /dev/null +++ b/docs/index.md @@ -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. diff --git a/docs/llm.md b/docs/llm.md new file mode 100644 index 0000000..1ebe862 --- /dev/null +++ b/docs/llm.md @@ -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. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..9987795 --- /dev/null +++ b/docs/usage.md @@ -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. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..7e5423e --- /dev/null +++ b/mkdocs.yml @@ -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 diff --git a/requirements-dev.txt b/requirements-dev.txt index a8e417c..1822e87 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,5 @@ pytest>=8.2 pytest-asyncio>=1.2 pytest-timeout>=2.4 + +mkdocs-material>=9.5