# show(data) Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add `show(data)` as painted's zero-config display entry point — one function that auto-detects context and renders any Python value.

**Architecture:** `show()` composes three existing pieces: `detect_context()` for TTY/format resolution, `shape_lens()` (or custom lens) for rendering data to Block, and `print_block()` for output. Three paths: Block passthrough, JSON serialization (pipe), styled rendering (TTY).

**Tech Stack:** Pure Python, no new dependencies. Builds on painted.fidelity, painted._lens, painted.writer.

**Design doc:** `docs/plans/2026-02-25-show-design.md`

---

### Task 1: Test — Block passthrough path

**Files:**
- Create: `tests/test_show.py`

**Step 1: Write the failing test**

```python
"""Tests for show() — zero-config display entry point."""

import io

from painted import Block, Style, show


class TestShowBlock:
    """show() with a pre-built Block passes it through to print_block."""

    def test_block_passthrough_outputs_content(self):
        """show(block) prints the block's content."""
        block = Block.text("hello", Style())
        buf = io.StringIO()
        show(block, file=buf)
        assert "hello" in buf.getvalue()

    def test_block_passthrough_ignores_lens(self):
        """show(block, lens=...) ignores the lens kwarg."""
        block = Block.text("direct", Style())
        buf = io.StringIO()
        # lens should not be called
        boom = lambda data, zoom, width: (_ for _ in ()).throw(AssertionError("lens called"))
        show(block, lens=boom, file=buf)
        assert "direct" in buf.getvalue()
```

**Step 2: Run test to verify it fails**

Run: `uv run --package painted pytest tests/test_show.py -v`
Expected: ImportError — `cannot import name 'show' from 'painted'`

**Step 3: Commit**

```bash
git add tests/test_show.py
git commit -m "test: add failing tests for show() Block passthrough"
```

---

### Task 2: Test — JSON path (piped output)

**Files:**
- Modify: `tests/test_show.py`

**Step 1: Write the failing test**

Append to `tests/test_show.py`:

```python
import json


class TestShowJSON:
    """show() with Format.JSON serializes data directly."""

    def test_json_dict(self):
        """show(dict, format=JSON) outputs valid JSON."""
        from painted import Format
        buf = io.StringIO()
        show({"status": "ok", "count": 42}, format=Format.JSON, file=buf)
        parsed = json.loads(buf.getvalue())
        assert parsed["status"] == "ok"
        assert parsed["count"] == 42

    def test_json_list(self):
        """show(list, format=JSON) outputs valid JSON."""
        from painted import Format
        buf = io.StringIO()
        show([1, 2, 3], format=Format.JSON, file=buf)
        parsed = json.loads(buf.getvalue())
        assert parsed == [1, 2, 3]

    def test_json_non_serializable_uses_str(self):
        """show() with non-serializable data falls back to str()."""
        from painted import Format
        from datetime import datetime
        buf = io.StringIO()
        dt = datetime(2026, 2, 25, 12, 0, 0)
        show({"when": dt}, format=Format.JSON, file=buf)
        parsed = json.loads(buf.getvalue())
        assert "2026" in parsed["when"]
```

**Step 2: Run test to verify it fails**

Run: `uv run --package painted pytest tests/test_show.py::TestShowJSON -v`
Expected: ImportError — `cannot import name 'show' from 'painted'`

**Step 3: Commit**

```bash
git add tests/test_show.py
git commit -m "test: add failing tests for show() JSON path"
```

---

### Task 3: Test — Rendered path (shape_lens default + custom lens)

**Files:**
- Modify: `tests/test_show.py`

**Step 1: Write the failing test**

Append to `tests/test_show.py`:

```python
from painted import Zoom


class TestShowRendered:
    """show() renders data through lens and prints the Block."""

    def test_dict_renders_via_shape_lens(self):
        """show(dict) uses shape_lens by default and produces output."""
        buf = io.StringIO()
        show({"name": "alice", "age": 30}, file=buf)
        output = buf.getvalue()
        assert len(output) > 0  # shape_lens produced something

    def test_custom_lens(self):
        """show(data, lens=fn) uses the provided lens."""
        def my_lens(data, zoom, width):
            return Block.text(f"custom:{data}", Style())

        buf = io.StringIO()
        show(42, lens=my_lens, file=buf)
        assert "custom:42" in buf.getvalue()

    def test_zoom_passed_to_lens(self):
        """show(data, zoom=X) passes zoom to the lens."""
        received = {}

        def spy_lens(data, zoom, width):
            received["zoom"] = zoom
            return Block.text("ok", Style())

        buf = io.StringIO()
        show("hello", zoom=Zoom.DETAILED, lens=spy_lens, file=buf)
        assert received["zoom"] == Zoom.DETAILED

    def test_string_renders(self):
        """show(str) renders the string."""
        buf = io.StringIO()
        show("hello world", file=buf)
        assert "hello" in buf.getvalue()
```

**Step 2: Run test to verify it fails**

Run: `uv run --package painted pytest tests/test_show.py::TestShowRendered -v`
Expected: ImportError — `cannot import name 'show' from 'painted'`

**Step 3: Commit**

```bash
git add tests/test_show.py
git commit -m "test: add failing tests for show() rendered path"
```

---

### Task 4: Implement show()

**Files:**
- Modify: `src/painted/__init__.py` (add `show` function + export)

**Step 1: Implement show()**

Add to `src/painted/__init__.py`, after the existing imports:

```python
import json
import sys
from typing import Any, Callable, TextIO

def show(
    data: Any,
    *,
    zoom: Zoom = Zoom.SUMMARY,
    lens: Callable[[Any, int, int], "Block"] | None = None,
    format: Format = Format.AUTO,
    file: TextIO = sys.stdout,
) -> None:
    """Display data with auto-detected formatting.

    Three paths:
    - Block: print directly via print_block
    - JSON format (piped or explicit): json.dumps with default=str
    - Otherwise: render through lens (default shape_lens) then print_block

    Args:
        data: Any Python value, or a pre-built Block.
        zoom: Detail level (default SUMMARY).
        lens: Render function override (default: shape_lens).
        format: Force output format (default: auto-detect from TTY).
        file: Output stream (default: sys.stdout).
    """
    from ._lens import shape_lens
    from .fidelity import detect_context, OutputMode, _setup_defaults
    from .writer import print_block

    # Block passthrough — already rendered
    if isinstance(data, Block):
        ctx = detect_context(zoom, OutputMode.AUTO, format)
        _setup_defaults(ctx)
        print_block(data, file, use_ansi=(ctx.format == Format.ANSI))
        return

    # Detect output context
    ctx = detect_context(zoom, OutputMode.AUTO, format)
    _setup_defaults(ctx)

    # JSON path — serialize directly
    if ctx.format == Format.JSON:
        file.write(json.dumps(data, default=str))
        file.write("\n")
        file.flush()
        return

    # Rendered path — lens to Block, then print
    render_fn = lens or shape_lens
    block = render_fn(data, ctx.zoom, ctx.width)
    print_block(block, file, use_ansi=(ctx.format == Format.ANSI))
```

Add `"show"` to the `__all__` list.

**Step 2: Run all show tests**

Run: `uv run --package painted pytest tests/test_show.py -v`
Expected: All PASS

**Step 3: Run full test suite**

Run: `uv run --package painted pytest tests/ -q`
Expected: All pass, no regressions

**Step 4: Commit**

```bash
git add src/painted/__init__.py
git commit -m "feat: add show() zero-config display entry point"
```

---

### Task 5: Test — auto-detect format from TTY state

**Files:**
- Modify: `tests/test_show.py`

**Step 1: Write the test**

Append to `tests/test_show.py`:

```python
class TestShowAutoDetect:
    """show() auto-detects format from TTY state."""

    def test_non_tty_stream_uses_plain(self):
        """Writing to a non-TTY stream produces plain text (no ANSI codes)."""
        buf = io.StringIO()
        show({"key": "value"}, file=buf)
        output = buf.getvalue()
        # StringIO.isatty() returns False → format resolves to PLAIN
        assert "\x1b[" not in output  # no ANSI escape codes

    def test_format_json_override(self):
        """format=JSON forces JSON output regardless of TTY."""
        from painted import Format
        buf = io.StringIO()
        show({"key": "value"}, format=Format.JSON, file=buf)
        parsed = json.loads(buf.getvalue())
        assert parsed["key"] == "value"

    def test_format_plain_override(self):
        """format=PLAIN forces plain text (no ANSI)."""
        from painted import Format
        buf = io.StringIO()
        show({"key": "value"}, format=Format.PLAIN, file=buf)
        output = buf.getvalue()
        assert "\x1b[" not in output
```

**Step 2: Run tests**

Run: `uv run --package painted pytest tests/test_show.py::TestShowAutoDetect -v`
Expected: All PASS (implementation already handles this)

**Step 3: Commit**

```bash
git add tests/test_show.py
git commit -m "test: add auto-detect format tests for show()"
```

---

### Task 6: Run full suite and verify

**Step 1: Run full test suite**

Run: `uv run --package painted pytest tests/ -q`
Expected: All pass (previous count + new show tests)

**Step 2: Quick smoke test**

Run: `uv run --package painted python -c "from painted import show; show({'name': 'alice', 'scores': [95, 87, 92]})"`
Expected: Styled output in terminal (or plain text if piped)

Run: `uv run --package painted python -c "from painted import show; show({'name': 'alice', 'scores': [95, 87, 92]})" | cat`
Expected: Plain text output (pipe detected)

Run: `uv run --package painted python -c "from painted import show, Format; show({'name': 'alice', 'scores': [95, 87, 92]}, format=Format.JSON)"`
Expected: `{"name": "alice", "scores": [95, 87, 92]}`
