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.
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:
$ MY_AUTO= EXEV= ./cli.py
auto='' type(auto)=<class 'str'>
ex=None type(ex)=<class 'NoneType'>
Expected output:
$ 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:
|
def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]: |
|
if self.envvar is None: |
|
return None |
|
|
|
if isinstance(self.envvar, str): |
|
rv = os.environ.get(self.envvar) |
|
|
|
if rv: |
|
return rv |
|
else: |
|
for envvar in self.envvar: |
|
rv = os.environ.get(envvar) |
|
|
|
if rv: |
|
return rv |
|
|
|
return None |
(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:
|
if ( |
|
self.allow_from_autoenv |
|
and ctx.auto_envvar_prefix is not None |
|
and self.name is not None |
|
): |
|
envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" |
|
rv = os.environ.get(envvar) |
The most obvious fix would be to add the following 2 lines
after
|
rv = os.environ.get(envvar) |
There are 2 ways of configuring Click to load a value of a specific option from an environment variable: "automatic" via
auto_envvar_prefixand "explicit" via passingenvvarkeyword 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 (Noneif not configured otherwise). But when a name is inferred fromauto_envvar_prefixconfiguration, Click treats empty string values as valid values, does not set a default and passes the option toParamType.convert()iftypekeyword argument is specified for that option.This is inconsistent.
Actual output:
Expected output:
Environment:
3.9.78.0.3It's being a nuisance for quite a while at my company, we have
.env.examplefile with variables containing multipleexport FOO=lines for each project. When my colleagues work on the project, they just copy.env.exampleto.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 validatorsTypeInstance.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:
click/src/click/core.py
Lines 2267 to 2283 in 6411f42
(i.e. if value is an empty string,
Noneis returned)But then here it is fetched as is without checking for an empty string:
click/src/click/core.py
Lines 2777 to 2783 in 6411f42
The most obvious fix would be to add the following 2 lines
after
click/src/click/core.py
Line 2783 in 6411f42