HBNetwork / python-decouple

Strict separation of config from code.
MIT License
2.83k stars 196 forks source link

TypeError if `default` can't go through `cast` #100

Closed MicaelJarniac closed 4 years ago

MicaelJarniac commented 4 years ago

In my use case, I have some configuration variables that are optional, and some of them have a specific type.

As they're optional, they can either be of some specific type, or None. The problem is, I use cast=type when reading them, but if I add default=None, then cast returns an error, as None can't be cast to type.

Example

Say I have a project for reading a sensor, and there's an optional config var for triggering an alarm if the sensor reaches a certain level.

Such config var should be a float, if set, and it can be positive, zero, or negative, as the values read from the sensor can be any float.

If I don't want the alarm feature, I can set its threshold to None, and that bypasses the checks completely.

The problem is, if I have the config being cast to float, and the default value set to None, it tries to cast None to float and errors.

So I have to either have no cast, so I'm able to assign None to it, or I have to do some try, except logic to handle the variable not being present, thus creating an alternative to the default parameter.

Having the default value be a float, like 0.0 or -1.0, wouldn't help in this case, as the sensor can output those values, thus it'd be a valid threshold and the alarm could be triggered.

My current workaround is as follows:

from decouple import config, UndefinedValueError

try:
    THRESHOLD = config("THRESHOLD", cast=float)
except UndefinedValueError:
    THRESHOLD = None

This way, if THRESHOLD is not set, it'll raise the UndefinedValueError, that'll then be handled by the except block, and set THRESHOLD to None.

It seems to do the trick, but when there are multiple variables like this one, it can easily become a mess.

My suggestion is to have something like an optional parameter, that's False by default, but that can be set to True, and if so, if the value is not present, instead of raising an exception, it returns None.

The same example from above would be like this:

from decouple import config

THRESHOLD = config("THRESHOLD", optional=True, cast=float)

That's considerably less code, and it's more readable IMO.

It's a bit similar to how typing does things:

from typing import Union

def alarm(reading: float, thresh: Union[float, None] = None) -> bool:
    if thresh is not None:
        if reading > thresh:
            return True
    return False

is the same as

from typing import Optional

def alarm(reading: float, thresh: Optional[float] = None) -> bool:
    if thresh is not None:
        if reading > thresh:
            return True
    return False
MicaelJarniac commented 4 years ago

I just realized I can probably create a custom function to give to cast, and that might do the trick for the time being.

But I still think the feature I suggested would be nice.

MicaelJarniac commented 4 years ago

Proof of concept:

from decouple import config

class Optional(object):

    def __init__(self, cast=str):
        self.cast = cast

    def __call__(self, value):
        if value is None:
            return None
        elif value == "None" or value == "":
            return None
        else:
            return self.cast(value)

THRESHOLD = config("THRESHOLD", default=None, cast=Optional(float))

This way, THRESHOLD in the config file can be missing, empty or the string None, and all result in config returning None. If THRESHOLD is set, however, it's then cast to the provided type (float, in this case).

It's not perfect, but it does work sort of how I wanted it to.

But still, when calling config, we have to provide a default, otherwise it'll still throw the error if it can't find the value in the config. It'd be nice to be able to do just

from decouple import config, Optional

THRESHOLD = config("THRESHOLD", cast=Optional(float))

or even

from decouple import config

THRESHOLD = config("THRESHOLD", optional=True, cast=float)
henriquebastos commented 4 years ago

Default must always be a string to be parsed by cast. Decouple is small and simple. Any workflow related code must be on your project's level, not the library.