rylai 0.1.0

Generate Python .pyi stub files from pyo3-annotated Rust source code statically without compilation
# Rylai

[![CI](https://github.com/monchin/Rylai/actions/workflows/ci.yml/badge.svg)](https://github.com/monchin/Rylai/actions/workflows/ci.yml)

Generate Python `.pyi` stub files from [pyo3](https://github.com/PyO3/pyo3)-annotated Rust source code — **statically, without compilation**.

## Features

- Parses `#[pymodule]`, `#[pyfunction]`, and `#[pyclass]` annotations directly from Rust source
- Maps Rust types to Python types automatically (`i32``int`, `Vec<T>``list[T]`, `Option<T>``T | None`, etc.)
- Extracts doc comments and emits them as Python docstrings
- Generates one `.pyi` file per top-level `#[pymodule]`
- Python-version-aware output (`T | None` for ≥ 3.10, `Optional[T]` for older)
- Zero-config by default; optionally configured via `rylai.toml`

## Why Rylai?

Compared with other tools that generate `.pyi` stubs for PyO3 projects, Rylai offers:

- **No compilation** — Rylai parses Rust source code directly (via [syn]https://github.com/dtolnay/syn). You don’t need to build the crate or depend on compiled artifacts, so stub generation is fast and works even when the project doesn’t compile (e.g. missing native deps or wrong toolchain).
- **No code changes** — No need to add build scripts, `#[cfg]` blocks, or extra annotations to your Rust code. Point Rylai at your crate root and it reads existing `#[pymodule]` / `#[pyfunction]` / `#[pyclass]` as-is.
- **No Python version lock-in** — Stubs are plain text. You generate them once and use them with any Python version; there’s no dependency on a specific Python interpreter or ABI, so you avoid “built for Python 3.x” issues and cross-version workflows stay simple.

Together, this makes Rylai easy to integrate into CI, docs, or local dev without touching your PyO3 code or your Python environment.

## Installation

Choose one of the following:

| Method | Command | Notes |
|--------|---------|--------|
| **Cargo** | `cargo install rylai` | Build from source and install to `~/.cargo/bin` |
| **uv** | `uv tool install rylai` | Install to uv tools dir; requires [publish to PyPI]https://pypi.org/project/rylai/ first |
| **uvx** | `uvx rylai` | Run without installing (same as uv; requires PyPI release) |
| **crgx** | `crgx rylai` | Run pre-built binary without compiling; requires [crgx]https://github.com/yfedoseev/crgx and a GitHub Release |

For local development:

```bash
cargo install --path .
```

## Usage

The path you pass is the **project root** — the folder that contains `Cargo.toml` (and usually a `src/` directory). Rylai scans all `.rs` files under that project’s `src/` and uses the root for `rylai.toml`, `pyproject.toml`, etc.

```bash
# Run in the current directory (must be the project root with Cargo.toml)
rylai

# Specify the project root explicitly (folder containing Cargo.toml)
rylai path/to/my_crate

# Write stubs to a custom output directory
rylai path/to/my_crate --output path/to/out/

# Use a custom config file
rylai --config path/to/rylai.toml
```

### For developers (this repo)

You don’t need to install the binary. Use **`cargo run`** and pass arguments after `--`:

```bash
# Generate stubs for the example crate (writes into examples/pyo3_sample/)
cargo run -- examples/pyo3_sample

# Same as above, with explicit output directory
cargo run -- examples/pyo3_sample --output examples/pyo3_sample

# Show help
cargo run -- --help
```

Anything after `--` is forwarded to the `rylai` binary.

### Example

Given this Rust source (`src/lib.rs`):

```rust
use pyo3::prelude::*;

#[pymodule]
mod pyo3_sample {
    use pyo3::prelude::*;

    /// Formats the sum of two numbers as string.
    #[pyfunction]
    fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
        Ok((a + b).to_string())
    }
}
```

Rylai produces `pyo3_sample.pyi`:

```python
# Auto-generated by rylai. Do not edit manually.

# Module: pyo3_sample
def sum_as_string(a: int, b: int) -> str:
    """Formats the sum of two numbers as string."""
```

## Configuration

Place a `rylai.toml` in your crate root to customize behavior. All sections are optional.

```toml
[output]
# Target Python version — affects Optional[T] vs T | None syntax (default: "3.10")
python_version = "3.10"

# Prepend auto-generated header comment (default: true)
add_header = true

[fallback]
# What to emit when a type cannot be resolved statically:
#   "any"   — emit Any and print a warning (default)
#   "error" — abort with an error
#   "skip"  — silently omit the item
strategy = "any"

[features]
# cfg features to treat as active during parsing
enabled = ["some_feature"]

[type_map]
# Custom Rust type → Python type overrides
"numpy::PyReadonlyArray1" = "numpy.ndarray"
"numpy::PyReadonlyArray2" = "numpy.ndarray"

[[override]]
# Manually written stub for a specific item (takes precedence over generated output)
item = "my_module::complex_function"
stub = "def complex_function(x: Any, **kwargs: Any) -> dict[str, Any]: ..."
```

## Supported Type Mappings

| Rust type | Python type |
|---|---|
| `i8``usize` | `int` |
| `f32`, `f64` | `float` |
| `bool` | `bool` |
| `str`, `String` | `str` |
| `()` | `None` |
| `Option<T>` | `T \| None` / `Optional[T]` |
| `Vec<T>` | `list[T]` |
| `HashMap<K,V>`, `BTreeMap<K,V>` | `dict[K, V]` |
| `HashSet<T>`, `BTreeSet<T>` | `set[T]` |
| `PyResult<T>` | `T` (errors become Python exceptions) |
| `Py<T>`, `Bound<T>` | recurse into `T` |
| Unknown types | `Any` (configurable) |

## Contributing

Before committing, run the pre-commit checks with [prek](https://github.com/j178/prek). See [CONTRIBUTING.md](CONTRIBUTING.md) for details.

## License

[LICENSE](LICENSE)