# click-constrained-option

**click-constrained-option** is an extension package that adds constrains to option in [Click](https://github.com/pallets/click/).

Tested under click 7.x

## What it does

Build option that: 
* mutually exclusive with others
* depend on other options
* is required conditionally
* is promoted conditionally
* set its default value conditionally
* set its type conditionally

## Quick start

```bash
pip install click-constrained-option
```

```python
# example.py

import click
from click_constrained_option import ConstrainedOption

@click.command()
@click.option(cls=ConstrainedOption,
              group_require_one=["apple", "orange", "pear"])
@click.option("--apple",
              cls=ConstrainedOption,
              is_flag=True)
@click.option("--orange",
              cls=ConstrainedOption,
              is_flag=True)
@click.option("--pear",
              cls=ConstrainedOption,
              is_flag=True)
def cli(**kwargs):
    click.echo(kwargs)


if __name__ == "__main__":
    cli()

```
```shell script
$ python example.py

Error: require exact one of '--apple' '--orange' '--pear'

$ python example.py --apple --pear

Error: require exact one of '--apple' '--orange' '--pear'
```

## API

|kwarg|type|usage|
|:---|:---|:---|
|allowed_func|`function`|return True to indicate allowance, vice versa
|allowed_if|`str`|name of the option
|allowed_if_not|`str`|name of the option
|allowed_if_all_of|`list`|list of the option names
|allowed_if_none_of|`list`|list of the option names
|allowed_if_any_of|`list`|list of the option names
|allowed_if_one_of|`list`|list of the option names
|required_func|`function`|return True to indicate requirement, vice versa
|required_if|`str`|name of the option
|required_if_not|`str`|name of the option
|required_if_all_of|`list`|list of the option names
|required_if_none_of|`list`|list of the option names
|required_if_any_of|`list`|list of the option names
|required_if_one_of|`list`|list of the option names
|prompt_func|`function`|return True to indicate prompt, vice versa
|prompt_if|`str`|name of the option
|prompt_if_not|`str`|name of the option
|prompt_if_all_of|`list`|list of the option names
|prompt_if_none_of|`list`|list of the option names
|prompt_if_any_of|`list`|list of the option names
|prompt_if_one_of|`list`|list of the option names
|group_require_one|`list`|list of the option names
|group_require_any|`list`|list of the option names
|group_require_all|`list`|list of the option names
|default_func|`function`|return a value to use as the default
|type_func|`function`|return a valid type of the option

### Note

* If multiple kwargs are used, then all of them must be satisfied. For example, if `prompt_if` and `prompt_if_all_of` are specified, then the option is prompted only when both condition hold true.  

* If the custom function has an argument equals an option name in the command, then the value of that option will be used as argument. For example, value of `--foo` will be passed to a custom function `bar(foo)`. 

## Examples

### Dependency and exclusive relation

`--username` is allowed if `--login` is set:
```python
import click
from click_constrained_option import ConstrainedOption

@click.command()
@click.option("--username",
              cls=ConstrainedOption,
              allowed_if="login")
@click.option("--login", is_flag=True)
def cli(**kwargs):
    click.echo(kwargs)


if __name__ == "__main__":
    cli()
```
```shell script
$ python example.py --username=foo

Error: Usage for '--username': require '--login'
```

`--login` is allowed if at least one of `--userid` and `--email` is specified, and `--oauth` is not set:
```python
import click
from click_constrained_option import ConstrainedOption

@click.command()
@click.option("--login",
              cls=ConstrainedOption,
              is_flag=True,
              allowed_if_any_of=["userid", "email"],
              allowed_if_not="cookie")
@click.option("--userid")
@click.option("--email")
@click.option("--oauth", is_flag=True)
def cli(**kwargs):
    click.echo(kwargs)


if __name__ == "__main__":
    cli()
```
```shell script
$ python example.py --login --userid=foo --oauth

Error: Usage for '--login': conflict with '--oauth'

$ python example.py --login

Error: Usage for '--login': require at least one of '--userid' '--email'
```

`--port` is required if `--listen` is an IP address:
```python
import click
import re
from click_constrained_option import ConstrainedOption

@click.command()
@click.option("--port",
              cls=ConstrainedOption,
              required_func=lambda b: re.match(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", b))
@click.option("--listen")
def cli(**kwargs):
    click.echo(kwargs)


if __name__ == "__main__":
    cli()
```
```shell script
$ python example.py --listen=127.0.0.1

Error: Missing option '--port'
```

### Mutually exclusive group

Exact one of the `--order-by-date` `--order-by-name` `--order-by-rank` must be set:
```python
import click
from click_constrained_option import ConstrainedOption

@click.option(cls=ConstrainedOption,
              group_require_one=["order_by_name", "order_by_rank", "order_by_date"])
@click.option("--order-by-name",
              cls=ConstrainedOption,
              is_flag=True)
@click.option("--order-by-rank",
              cls=ConstrainedOption,
              is_flag=True)
@click.option("--order-by-date",
              cls=ConstrainedOption,
              is_flag=True)
def cli(**kwargs):
    click.echo(kwargs)


if __name__ == "__main__":
    cli()
```
```shell script
$ python example.py

Error: require exact one of '--order-by-name' '--order-by-rank' '--order-by-date'
```

### Conditional prompt

`--password` will be prompted if one of `--userid` `--email` is specified

```python
import click
from click_constrained_option import ConstrainedOption

@click.command()
@click.option("--userid")
@click.option("--email")
@click.option("--password", 
              hide_input=True,
              prompt_if_one_of=["userid", "email"])
def cli(**kwargs):
    click.echo(kwargs)


if __name__ == "__main__":
    cli()
```
```shell script
$ python example.py --userid

Password: 
```

### Conditional type

Type of `--time` will be int if `--time-format=timestamp`

```python
import click
from click_constrained_option import ConstrainedOption

@click.command()
@click.option("--time-format",
              type=click.Choice(["iso-8601", "timestamp"]))
@click.option("--time",
              cls=ConstrainedOption,
              type_func=lambda time_format: click.INT if time_format == "timestamp" else click.STRING)
def cli(**kwargs):
    click.echo(kwargs)


if __name__ == "__main__":
    cli()
```

```shell script
$ python --time-format="timestamp" --time=str_not_int

Error: Invalid value for '--time': str_not_int is not a valid integer
```

### Conditional default

Default of `--lucky` is set through custom function

```python
import click
from click_constrained_option import ConstrainedOption
from random import randint

@click.command()
@click.option("--lucky",
              default=lambda : randint(0, 9))
def cli(**kwargs):
    click.echo(kwargs)


if __name__ == "__main__":
    cli()
```

```shell script
$ python example.py

{'lucky': '5'}
```