# r_bridge — Claude Code Guide

## Project overview

Python library that runs a persistent R subprocess and exposes a Pythonic API over it.
Communication uses line-delimited JSON with a `__RBRIDGE__:` sentinel prefix over stdin/stdout.

## Commands

```bash
uv run pytest              # run all tests
uv run pytest tests/test_bridge.py -v   # integration tests (spawn real R)
uv run pytest tests/test_serializer.py  # unit tests (no R needed)
```

## Architecture

```
r_bridge/
├── bridge.py          # RBridge class — lifecycle, send/recv, lock, timeouts, verbose mode
├── protocol.py        # Envelope construction and sentinel stripping
├── serializer.py      # Python/numpy/pandas → tagged JSON dict
├── deserializer.py    # Tagged JSON dict → Python/numpy/pandas
├── type_hints.py      # TypeHint enum (SCALAR, NUMPY, PANDAS, LIST, RAW)
├── exceptions.py      # RBridgeError hierarchy
├── utils.py           # drain_stderr (with optional on_line callback), get_r_executable
└── _r_scripts/
    └── bridge_loop.R  # R-side REPL — serializers, deserializers, main loop
```

## Key design decisions

- **Sentinel prefix** — `__RBRIDGE__:` on every protocol line; stray `cat()`/`print()` output is logged/printed but never corrupts the stream.
- **Single lock** — `threading.Lock` serializes all calls; one in-flight request at a time.
- **Stderr drain thread** — daemon thread continuously drains R's stderr to prevent pipe-buffer deadlocks; content exposed via `bridge.last_stderr`.
- **`simplifyVector=FALSE`** in `jsonlite::fromJSON` — prevents R from auto-simplifying JSON arrays, keeping deserialization unambiguous.
- **Locale** — subprocess started with `LANG=en_US.UTF-8`; R sets `options(OutDec=".")` so JSON numbers always use `.` as decimal separator.
- **`_BRIDGE_ATTRS` frozenset** — names in this set (and names starting with `_`) are routed to `object.__setattr__` instead of R; prevents internal attributes from leaking into R's global env.

## RBridge constructor parameters

| Parameter | Default | Purpose |
|---|---|---|
| `r_executable` | `None` | Path to `Rscript`; auto-detected from PATH if `None` |
| `startup_timeout` | `15.0` | Seconds to wait for R ready signal |
| `call_timeout` | `60.0` | Per-call timeout in seconds |
| `env` | `{}` | Extra environment variables for the R subprocess |
| `r_libs` | `[]` | Prepended to `R_LIBS_USER` |
| `log_level` | `"WARNING"` | Python `logging` level for the `r_bridge` logger |
| `verbose` | `False` | Print R calls, R stdout, and R stderr to `sys.stderr` in real time |

## Verbose mode output format

```
[R call] mean([1.0, 2.0, 3.0])   ← call_func ops
[R eval] 2 ^ 8                    ← eval_expr ops
[R exec] square <- function ...   ← exec_script ops (first line shown)
[R set]  x = ...                  ← set_var ops
[R get]  x                        ← get_var ops
[R stdout] <line>                 ← non-protocol R stdout (e.g. cat())
[R stderr] <line>                 ← R stderr (e.g. message(), warnings)
```

All verbose output goes to `sys.stderr`.

## Type conversion

Custom types can be registered without modifying the library:

```python
from r_bridge import register_serializer, register_deserializer
register_serializer(MyType, lambda obj: {"__type__": "mytag", ...})
register_deserializer("mytag", lambda d: MyType(...))
```

## Windows compatibility

- **R 4.2+ (UCRT build) required** — older MSVCRT builds lack system-level UTF-8 support; non-ASCII strings in R output may be corrupted.
- `R_LIBS_USER` uses `os.pathsep` (`;` on Windows, `:` on Unix) — set automatically.
- `LANG=en_US.UTF-8` is only set on non-Windows; Windows R uses Windows locale APIs.
- `subprocess.Popen` is opened with `encoding="utf-8"` explicitly to avoid the Windows system default (`cp1252`).

## Testing notes

- `tests/test_bridge.py` — integration tests; each test that needs its own bridge creates one inline (e.g. verbose tests). The shared module-scoped `bridge` fixture is for stateless or additive tests only.
- `tests/test_utils.py` — unit tests for `drain_stderr` and helpers; use real subprocesses, not mocks.
- Tests that use `capsys` to check verbose output must create their own `RBridge(verbose=True)` inside the test body — the shared fixture uses `verbose=False`.
- `pytest-timeout` is configured globally to 30 s (`pyproject.toml`); long-running R tests (e.g. `Sys.sleep`) use a short `call_timeout` on the bridge itself.
