# CLI Context: How Global Options Flow to Commands

## What is this guide?

This guide explains how Typer's **Context** object lets you:

- Define options globally (once in `main.py`)
- Access them in any command without redefining them
- Allow commands to override global options when needed

Perfect for beginners learning how Haniwers v1 CLI is organized!

## The Big Picture

Think of it like this: **Global options are shared with all commands through a messenger (Context).**

```
┌─────────────────────────────────────┐
│  Global Callback in main.py         │
│  (runs first, before any command)   │
│                                     │
│  - Load .env.haniwers               │
│  - Parse global --config option     │
│  - Parse global --verbose flag      │
│  - Save everything in ctx.obj       │
└────────────┬────────────────────────┘
             │ Context object carries the data
             │ (like a messenger bag)
             ▼
┌─────────────────────────────────────┐
│  Command (config show, daq, etc)    │
│  (runs second)                      │
│                                     │
│  - Receive ctx from main            │
│  - Access ctx.obj["config_path"]    │
│  - Can override with own --config   │
└─────────────────────────────────────┘
```

## How It Works: Step by Step

### Step 1: Define Global Options in `main.py`

```python
@app.callback()
def main(
    ctx: Context,  # ← This is the messenger
    env_path: Path = typer.Option(..., "--env", ...),
    config_path: Path = typer.Option(..., "--config", "-c", ...),
    verbose: bool = typer.Option(False, "--verbose", "-v", ...),
) -> None:
    """Global callback runs FIRST, before any command."""

    # Do setup
    load_dotenv(dotenv_path=env_path)
    configure_logging(verbose=verbose)

    # Store in ctx.obj (the messenger bag)
    ctx.obj = {
        "env_path": env_path,
        "config_path": config_path,
        "verbose": verbose
    }
```

**Key insight:** `ctx.obj` is just a **dictionary** that holds any data you want to share.

### Step 2: Receive Global Options in Commands

```python
@app.command(name="show")
def show_config(
    ctx: typer.Context,  # ← Receive the messenger
    config: Optional[Path] = typer.Option(None, "--config", "-c", ...),
) -> None:
    """Show configuration file contents."""

    # Access global options from ctx.obj
    global_config_path = ctx.obj.get("config_path")

    # Use fallback pattern: command-level > global > default
    config_path = config or global_config_path or "hnw.toml"

    print(f"Using config: {config_path}")
```

**Key insight:** `ctx.obj` is available to ANY command registered with the app.

## The Fallback Pattern

This is the most important concept. Commands can be called three ways:

### Pattern: Command-level option only

```bash
$ haniwers-v1 config show --config my.toml
# ↓
# config = Path("my.toml")
# global_config_path = None (or "hnw.toml")
# Result: uses "my.toml" (command-level wins)
```

### Pattern: Global option only

```bash
$ haniwers-v1 --config my.toml config show
# ↓
# config = None (command didn't specify it)
# global_config_path = Path("my.toml")
# Result: uses "my.toml" (global option applies)
```

### Pattern: No options (use defaults)

```bash
$ haniwers-v1 config show
# ↓
# config = None (command didn't specify it)
# global_config_path = None (global didn't specify it)
# Result: uses "hnw.toml" (hardcoded default)
```

### Pattern: Both specified (command-level wins)

```bash
$ haniwers-v1 --config global.toml config show --config local.toml
# ↓
# config = Path("local.toml") (command-level specified)
# global_config_path = Path("global.toml") (global specified)
# Result: uses "local.toml" (command-level takes precedence)
```

## Implementation Example

Here's the actual code pattern used in Haniwers:

### Bad Way (Doesn't work properly)

```python
# ✗ This DOESN'T inherit global options
@app.command(name="show")
def show_config(
    config: str = typer.Option("hnw.toml", "--config", "-c", ...),
) -> None:
    # Problem: Default "hnw.toml" is always truthy
    # So command never sees global option!
    pass
```

### Good Way (Inherits properly)

```python
# ✓ This DOES inherit global options
@app.command(name="show")
def show_config(
    ctx: typer.Context,  # ← Add Context parameter
    config: Optional[Path] = typer.Option(None, "--config", "-c", ...),
) -> None:
    """Display and validate your configuration file."""

    # Fallback chain: command > global > default
    config_path = config or ctx.obj.get("config_path") or "hnw.toml"

    # Now use config_path
    print(f"Using: {config_path}")
```

**Key differences:**

1. Add `ctx: typer.Context` as first parameter
2. Change default from `"hnw.toml"` to `None` (so we can tell if user specified it)
3. Use fallback chain with `or` operator

## Why `None` is Important

Understanding this is crucial for beginners:

```python
# When user doesn't specify an option, Typer sets it to:
# - The default value (if you provided one)
# - None (if no default)

# Example 1: Default is "hnw.toml"
config: str = typer.Option("hnw.toml", ...)
# If user doesn't specify --config:
#   config = "hnw.toml"  (always truthy!)
#   Never checks ctx.obj.get("config_path")

# Example 2: Default is None
config: Optional[str] = typer.Option(None, ...)
# If user doesn't specify --config:
#   config = None  (falsy - condition skips it)
#   Falls through to: ctx.obj.get("config_path") or default
```

**Remember:** Use `None` as default to enable proper fallback!

## Checking if Value Came from User

Sometimes you need to know: "Did the user specify this, or is it a default?"

```python
def my_command(
    ctx: typer.Context,
    count: Optional[int] = typer.Option(None, "--count", ...),
) -> None:
    """Example command."""

    # Check: did user specify --count?
    if count is not None:
        print(f"User specified: {count}")
    else:
        # Use global or default
        global_count = ctx.obj.get("count")
        if global_count is not None:
            count = global_count
            print(f"Using global: {count}")
        else:
            count = 10  # hardcoded default
            print(f"Using default: {count}")
```

**Key insight:** Only `None` means "not specified". Check with `is not None`, not truthiness.

## Common Mistakes

### Mistake 1: Forgetting `ctx` Parameter

```python
# ✗ WRONG - no ctx parameter
def show_config(
    config: str = typer.Option(None, ...),
) -> None:
    # Crash! AttributeError: 'NoneType' object has no attribute 'obj'
    path = ctx.obj.get("config_path")
```

**Fix:** Add `ctx: typer.Context` as first parameter

### Mistake 2: Using Hardcoded Default

```python
# ✗ WRONG - default prevents fallback
def show_config(
    ctx: typer.Context,
    config: str = typer.Option("hnw.toml", ...),  # Bad!
) -> None:
    # "hnw.toml" is always truthy
    # ctx.obj.get("config_path") never used
    path = config or ctx.obj.get("config_path") or "hnw.toml"
```

**Fix:** Use `None` as default

### Mistake 3: Using Truthiness for Numbers

```python
# ✗ WRONG - fails when value is 0
def my_command(
    ctx: typer.Context,
    count: Optional[int] = typer.Option(None, ...),
) -> None:
    if count:  # WRONG! Fails for count=0
        use_value = count
```

**Fix:** Use `is not None`

```python
# ✓ CORRECT - works for 0, -1, etc
if count is not None:  # RIGHT! Works for all numbers
    use_value = count
```

## The Three-Level Pattern

This is the standard pattern used throughout Haniwers v1 CLI:

```python
@app.command()
def my_command(
    ctx: typer.Context,
    # Option 1: Command-level (highest priority)
    my_option: Optional[str] = typer.Option(None, "--my-option", ...),
) -> None:
    """Example command with three-level fallback."""

    # Fallback chain (in order of priority):
    # 1. Command-level (if user specified --my-option)
    # 2. Global (from main callback in ctx.obj)
    # 3. Default (hardcoded in this command)
    final_value = (
        my_option
        or ctx.obj.get("my_option")
        or "default_value"
    )

    print(f"Using: {final_value}")
```

## Accessing Different Types of Global Data

```python
@app.command()
def my_command(ctx: typer.Context) -> None:
    """Access all types of global data."""

    # String values
    config_path = ctx.obj.get("config_path")

    # Boolean flags
    is_verbose = ctx.obj.get("verbose")

    # Paths
    env_path = ctx.obj.get("env_path")

    # Nested objects (if you store them)
    device_config = ctx.obj.get("device")

    # Check if key exists
    if "custom_key" in ctx.obj:
        value = ctx.obj["custom_key"]

    # Get with default if missing
    timeout = ctx.obj.get("timeout", 5.0)  # defaults to 5.0
```

## Storing Complex Objects in Context

You can store more than just strings:

```python
# In main.py callback
@app.callback()
def main(ctx: Context, ...):
    """Store complex objects in context."""

    # Load configuration
    loader = ConfigLoader(config_path)

    # Store everything in context
    ctx.obj = {
        "config_path": config_path,
        "config": loader.config,  # ← Store config object
        "device_config": loader.config.device,  # ← Store nested object
        "logger": configure_logger(verbose),  # ← Store logger
    }

# In command
@app.command()
def my_command(ctx: typer.Context) -> None:
    """Use complex objects from context."""

    # Access config object directly (no loading needed!)
    cfg = ctx.obj.get("config")
    print(f"Device port: {cfg.device.port}")

    # Access logger
    logger = ctx.obj.get("logger")
    logger.info("Command running")
```

## Testing Commands with Context

When writing tests, you need to provide a mock context:

```python
from typer.testing import CliRunner
from unittest.mock import MagicMock

runner = CliRunner()

def test_command_with_global_config():
    """Test that command uses global config."""

    # Create mock context
    mock_ctx = MagicMock()
    mock_ctx.obj = {
        "config_path": "/path/to/config.toml",
        "verbose": True,
    }

    # Run command - Typer provides mock_ctx automatically
    result = runner.invoke(app, ["my-command"])

    assert result.exit_code == 0
```

## Real Example: config show Command

Here's the actual implementation in Haniwers:

```python
# src/haniwers/v1/cli/config.py

@app.command(name="show")
def show_config(
    ctx: typer.Context,
    config: Optional[Path] = typer.Option(None, "--config", "-c", help="Path to config file"),
) -> None:
    """Display and validate your configuration file.

    Priority order:
    1. Command-level --config (if you provide it): highest priority
    2. Global --config (if you used global option): middle priority
    3. Default "hnw.toml" (if neither provided): fallback
    """

    # Fallback chain
    config_path = config or ctx.obj.get("config_path") or "hnw.toml"

    try:
        loader = ConfigLoader(config_path)
        json = loader.config.model_dump_json(indent=2)
        typer.echo(json)
    except Exception as e:
        msg = f"[ERROR] Failed to load config: {e}"
        typer.echo(msg, err=True)
        raise typer.Exit(code=1)
```

**Usage examples:**
```bash
# Use default
$ haniwers-v1 config show
# Uses: hnw.toml

# Use global option
$ haniwers-v1 --config my.toml config show
# Uses: my.toml (inherited from global)

# Override global with command-level
$ haniwers-v1 --config global.toml config show --config local.toml
# Uses: local.toml (command-level wins)
```

## Summary: Context Inheritance Pattern

| Item | Where | How to Access |
|------|-------|---------------|
| **Global options** | Defined in `main.py` callback | Stored in `ctx.obj` dictionary |
| **Pass to commands** | Via `@app.callback()` mechanism | Commands receive `ctx` parameter |
| **Command options** | Defined in command function | Use function parameters (default `None`) |
| **Precedence** | Command-level > Global > Hardcoded | Use fallback chain with `or` operator |
| **Testing** | Mock `ctx.obj` dictionary | Use `runner.invoke()` with test data |

## Next Steps

1. Look at `src/haniwers/v1/cli/main.py` - See global options defined
2. Look at `src/haniwers/v1/cli/config.py` - See commands using context
3. Look at `src/haniwers/v1/cli/daq.py` - See complex precedence logic
4. Write your own command following this pattern
5. Test with all three scenarios (command, global, default)

## For More Information

- [Typer Documentation: Context](https://typer.tiangolo.com/tutorial/commands/context/)
- [Haniwers CLI Development Guide](./cli-development.md)
- [Main entry point implementation](../src/haniwers/v1/cli/main.py)
