# Issue #1303: Possible issue with timeout in read_output_buffers

**GitHub Issue:** https://github.com/pyinfra-dev/pyinfra/issues/1303
**Reporter:** @goetzk
**Status:** Open
**Date Reported:** 2025-02-25

## Issue Summary

The `read_output_buffers` function in `pyinfra/connectors/util.py` has a type annotation indicating that `timeout` is `Optional[int]`, suggesting the parameter should be optional. However, the function signature defines `timeout` as a **positional parameter without a default value**, meaning it is effectively required despite the `Optional` type hint.

When a custom connector (like the reporter's LXD connector) tries to call `read_output_buffers` without providing a timeout argument, Python raises:

```
TypeError: read_output_buffers() missing 1 required positional argument: 'timeout'
```

## Root Cause Analysis

### The Problem

In `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/connectors/util.py`, the function is defined as:

```python
def read_output_buffers(
    stdout_buffer: Iterable,
    stderr_buffer: Iterable,
    timeout: Optional[int],       # <-- Optional type hint, but NO default value
    print_output: bool,
    print_prefix: str,
) -> CommandOutput:
```

The `Optional[int]` type annotation indicates that `timeout` can be either an `int` or `None`, which is a valid typing construct. However, from Python's function calling perspective, all parameters without default values are **required positional arguments**.

### Expected vs Actual Behavior

- **Expected:** The `timeout` parameter should be optional (with a default of `None`) so that external connectors can call `read_output_buffers` without specifying a timeout.
- **Actual:** The `timeout` parameter is required, and calling the function without it raises a `TypeError`.

### Internal Usage Analysis

Looking at how `read_output_buffers` is called internally:

1. **In `run_local_process` (util.py line 56-62):**
   ```python
   combined_output = read_output_buffers(
       process.stdout,
       process.stderr,
       timeout=timeout,
       print_output=print_output,
       print_prefix=print_prefix,
   )
   ```
   Here, `timeout` is passed from `run_local_process`'s own `timeout` parameter, which has a default of `None`.

2. **In `SSHConnector.run_shell_command` (ssh.py line 405-411):**
   ```python
   combined_output = read_output_buffers(
       stdout_buffer,
       stderr_buffer,
       timeout=_timeout,
       print_output=print_output,
       print_prefix=self.host.print_prefix,
   )
   ```
   Here, `_timeout` comes from `arguments.pop("_timeout", None)`, which defaults to `None`.

Both internal callers always pass `timeout` as a keyword argument, so they don't encounter the issue. The problem arises when external connector developers try to call `read_output_buffers` directly.

## Proposed Fix

### Primary Fix: Add Default Value to `timeout` Parameter

**File:** `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/connectors/util.py`

**Current code (line 146-152):**
```python
def read_output_buffers(
    stdout_buffer: Iterable,
    stderr_buffer: Iterable,
    timeout: Optional[int],
    print_output: bool,
    print_prefix: str,
) -> CommandOutput:
```

**Proposed change:**
```python
def read_output_buffers(
    stdout_buffer: Iterable,
    stderr_buffer: Iterable,
    timeout: Optional[int] = None,
    print_output: bool = False,
    print_prefix: str = "",
) -> CommandOutput:
```

### Rationale for Additional Default Values

While investigating the issue, I noticed that `print_output` and `print_prefix` also lack default values despite being commonly passed with consistent values. Adding defaults to these parameters would make the function more flexible for external connector developers:

- `print_output: bool = False` - Defaulting to not printing output is sensible
- `print_prefix: str = ""` - An empty prefix is a reasonable default

### Alternative Consideration

If the maintainers prefer to keep the function signature strict (requiring all parameters), the documentation and type hints should be updated to reflect this:

```python
def read_output_buffers(
    stdout_buffer: Iterable,
    stderr_buffer: Iterable,
    timeout: int | None,  # Change from Optional[int] to be clearer this is still required
    print_output: bool,
    print_prefix: str,
) -> CommandOutput:
```

And add a docstring explaining that all parameters are required even if `timeout` can accept `None`.

## Files That Need Modification

1. **Primary:** `/Users/nick/Dev/Webroot/pyinfra/src/src/pyinfra/connectors/util.py`
   - Line 149: Add `= None` default to `timeout` parameter
   - Optionally add defaults to `print_output` and `print_prefix`

2. **No other files need modification** - All existing callers use keyword arguments, so adding defaults will not break existing code.

## Testing Recommendations

1. Verify that existing tests pass after the change
2. Add a test case that calls `read_output_buffers` without the `timeout` argument
3. Verify the behavior when `timeout=None` (should wait indefinitely for greenlets)

## Additional Notes

The reporter is developing a custom LXD connector and encountered this issue when trying to use `read_output_buffers` as a utility function. This is a valid use case, and the function should be designed to be easily consumable by external connector implementations.

The `gevent.wait()` function that uses the timeout (line 179) already handles `None` correctly - it simply waits indefinitely without a timeout constraint. So the functional behavior when `timeout=None` is already correct; only the function signature needs adjustment.
