"""IO provider interface for pluggable input/output in prompt functions.
This module defines the IOProvider protocol and standard implementations
for handling user interaction in prompt functions. By using a pluggable
provider, prompts can be tested without monkeypatching builtins, and
alternative UIs (TUI, GUI) can be easily integrated.
"""
from __future__ import annotations
from typing import (
Protocol,
runtime_checkable,
)
@runtime_checkable
[docs]
class IOProvider(Protocol):
"""Protocol for pluggable input/output in prompt functions.
Implementations of this protocol can provide custom behavior for
displaying prompts, collecting input, and showing error messages.
This enables testing without mocking builtins and supports
alternative UIs beyond command-line interfaces.
Examples:
Use default builtin provider::
from valid8r.prompt.io_provider import BuiltinIOProvider
from valid8r.prompt.basic import ask
from valid8r.core.parsers import parse_int
provider = BuiltinIOProvider()
result = ask("Age: ", parser=parse_int, io_provider=provider)
Use test provider for non-interactive testing::
from valid8r.prompt.io_provider import TestIOProvider
test_provider = TestIOProvider(inputs=["25"])
result = ask("Age: ", parser=parse_int, io_provider=test_provider)
# result.value_or(0) == 25
# test_provider.outputs == []
# test_provider.errors == []
"""
[docs]
def output(self, message: str) -> None:
"""Display an output message to the user.
Args:
message: The message to display
"""
...
[docs]
def error(self, message: str) -> None:
"""Display an error message to the user.
Args:
message: The error message to display
"""
...
[docs]
class BuiltinIOProvider:
"""Default IO provider using Python builtins (input/print).
This provider delegates to Python's built-in input() and print()
functions, providing standard command-line interaction behavior.
Examples::
from valid8r.prompt.io_provider import BuiltinIOProvider
provider = BuiltinIOProvider()
# Input from user
user_input = provider.input("Name: ") # Uses input()
# Output to console
provider.output("Hello!") # Uses print()
# Error to console
provider.error("Invalid input") # Uses print()
"""
[docs]
def output(self, message: str) -> None:
"""Display output using builtins.print.
Args:
message: The message to display
"""
print(message)
[docs]
def error(self, message: str) -> None:
"""Display error using builtins.print.
Args:
message: The error message to display
"""
print(message)
[docs]
class TestIOProvider:
"""IO provider for testing that captures I/O without builtins.
This provider allows testing prompt functions without monkeypatching
builtins.input and builtins.print. It provides pre-configured inputs
and captures all outputs and errors for inspection.
Attributes:
inputs: List of input strings to return (consumed in order)
outputs: List of captured output messages
errors: List of captured error messages
Examples:
>>> from valid8r.prompt.io_provider import TestIOProvider
>>> from valid8r.prompt.basic import ask
>>> from valid8r.core.parsers import parse_int
>>>
>>> # Set up test provider with simulated inputs
>>> provider = TestIOProvider(inputs=["42", "invalid", "25"])
>>>
>>> # First call returns "42"
>>> result1 = ask("Age: ", parser=parse_int, io_provider=provider)
>>> # result1.value_or(0) == 42
>>>
>>> # Second call with retry consumes "invalid" then "25"
>>> result2 = ask("Age: ", parser=parse_int, retry=1, io_provider=provider)
>>> # result2.value_or(0) == 25
>>>
>>> # Inspect captured output
>>> len(provider.errors) # 1 error for "invalid"
1
>>> len(provider.outputs) # No outputs
0
"""
def __init__(self, inputs: list[str]) -> None:
"""Initialize test provider with simulated inputs.
Args:
inputs: List of input strings to return in sequence
"""
[docs]
self.outputs: list[str] = []
[docs]
self.errors: list[str] = []
[docs]
def output(self, message: str) -> None:
"""Capture output message.
Args:
message: The output message to capture
"""
self.outputs.append(message)
[docs]
def error(self, message: str) -> None:
"""Capture error message.
Args:
message: The error message to capture
"""
self.errors.append(message)