Repository: pallets/click (python)

Issue:
Inconsistent handling of option values from environment variables
There are 2 ways of configuring Click to load a value of a specific option from an environment variable: "automatic" via `auto_envvar_prefix` and "explicit" via passing `envvar` keyword argument to the option definition.

When a name of an environment variable is set explicitly, Click treats empty string values (i.e. `export MY_OPTION=`) as not set and keeps a default value (`None` if not configured otherwise). But when a name is inferred from `auto_envvar_prefix` configuration, Click treats empty string values as valid values, does not set a default and passes the option to `ParamType.convert()` if `type` keyword argument is specified for that option.

This is inconsistent.

```python
import click

@click.command()
@click.option('--auto')
@click.option('--ex', envvar="EXEV")
def cli(auto, ex):
    click.echo(f"{auto=} {type(auto)=}")
    click.echo(f"{ex=} {type(ex)=}")

if __name__ == '__main__':
    cli(auto_envvar_prefix='MY')
```

Actual output:

```shell
$ MY_AUTO= EXEV= ./cli.py
auto='' type(auto)=<class 'str'>
ex=None type(ex)=<class 'NoneType'>
```

Expected output:

```shell
$ MY_AUTO= EXEV= ./cli.py
auto=None type(auto)=<class 'NoneType'>
ex=None type(ex)=<class 'NoneType'>
```

Environment:

- Python version: `3.9.7`
- Click version: `8.0.3`

It's being a nuisance for quite a while at my company, we have `.env.example` file with variables containing multiple `export FOO=` lines for each project. When my colleagues work on the project, they just copy `.env.example` to `.env`, fill out required values (not all of the values) and then do a test run.

Then for variables like export `OPTIONAL_VALUE=` that are left as is, click loads them as empty strings and calls type validators `TypeInstance.convert()` with that value, which usually fails as an empty string is not a valid value for the majority of options.

Now when I go diving into the code base, it seems like it's handled "correctly" here:
https://github.com/pallets/click/blob/6411f425fae545f42795665af4162006b36c5e4a/src/click/core.py#L2267-L2283

(i.e. if value is an empty string, `None` is returned)

But then here it is fetched as is without checking for an empty string:
https://github.com/pallets/click/blob/6411f425fae545f42795665af4162006b36c5e4a/src/click/core.py#L2777-L2783

The most obvious fix would be to add the following 2 lines
``` python
if rv:
    return rv
```
after
https://github.com/pallets/click/blob/6411f425fae545f42795665af4162006b36c5e4a/src/click/core.py#L2783

Pull request:
treat empty auto_envvar as None
#1687 fixed empty value in env var specified with `envvar`, but missed that the same logic needed to happen for option `auto_envvar`.

fixes #2146

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 191937d..55fa41a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -52,6 +52,9 @@ Version 8.1.0
     ``name``. :issue:`2168`
 -   Shell completion prioritizes option values with option prefixes over
     new options. :issue:`2040`
+-   Options that get an environment variable value using
+    ``autoenvvar_prefix`` treat an empty value as ``None``, consistent
+    with a direct ``envvar``. :issue:`2146`
 
 
 Version 8.0.4
diff --git a/src/click/core.py b/src/click/core.py
index 939ad81..18431cd 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -2834,7 +2834,10 @@ class Option(Parameter):
             envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
             rv = os.environ.get(envvar)
 
-        return rv
+            if rv:
+                return rv
+
+        return None
 
     def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]:
         rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx)


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
index 3beff11..2e1f291 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -153,14 +153,15 @@ def test_init_bad_default_list(runner, multiple, nargs, default):
         click.Option(["-a"], type=type, multiple=multiple, nargs=nargs, default=default)
 
 
-def test_empty_envvar(runner):
+@pytest.mark.parametrize("env_key", ["MYPATH", "AUTO_MYPATH"])
+def test_empty_envvar(runner, env_key):
     @click.command()
     @click.option("--mypath", type=click.Path(exists=True), envvar="MYPATH")
     def cli(mypath):
         click.echo(f"mypath: {mypath}")
 
-    result = runner.invoke(cli, [], env={"MYPATH": ""})
-    assert result.exit_code == 0
+    result = runner.invoke(cli, env={env_key: ""}, auto_envvar_prefix="AUTO")
+    assert result.exception is None
     assert result.output == "mypath: None\n"