Skip to content

Inconsistent handling of option values from environment variables #2146

Description

@ddnomad

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:

click/src/click/core.py

Lines 2267 to 2283 in 6411f42

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:

click/src/click/core.py

Lines 2777 to 2783 in 6411f42

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

if rv:
    return rv

after

rv = os.environ.get(envvar)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

parsingParsing, parameters, commands, chaining, context

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions