Default for missing nested config should be the default state of the nested config
Closed this issue · 7 comments
Expected Behavior
Given the following:
from textwrap import dedent
from file_config import config, var, build_schema, utils, validate, to_dict
@config
class TestConfig:
@config
class InnerConfig:
foo = var(str, default="Default", required=False)
inner = var(InnerConfig, required=False)
bar = var(bool, default="Default", required=False)
json_ = dedent(
"""\
{
"bar": false
}
"""
)
c = TestConfig.loads_json(json_)
print(c)
I would expect the value of the parsed config instance would be TestConfig(inner=TestConfig.InnerConfig(foo="Default"), bar=False)
.
Current Behavior
Given the above example, the loaded TestConfig
instance is TestConfig(inner=None, bar=False)
.
Possible Solution
The solution that would solve this case is by changing the handler for building config types in _file_config._build
...
elif is_config_type(entry.type):
if arg_key not in dictionary:
- kwargs[var.name] = arg_default
+ kwargs[var.name] = (
+ _build(entry.type, {}) if arg_default is None else arg_default
+ )
else:
kwargs[var.name] = _build(
entry.type, dictionary.get(arg_key, arg_default)
)
@azazel75 this is related to #24. You mentioned you expect that the build config instance to be None
for missing nested configs. My initial/desired implementation is to have the "default state" of the inner config to be defined.
The proposed diff would ensure this as long as default is not None
for the inner = var(InnerConfig, required=False)
then the nested config will be built with its "empty" state. This will cause issues if you ever truly expect the built inner config to be None
.
The other solution is to internally default config vars to some kind of custom type which is interpreted like javascript's undefined
.
Your Environment
- File Config Version: 0.3.6
- Python Version: 3.7.1
- Operating System and Version: LUbuntu 18.10
- Link to your branch (if applicable):
❤️ Thank you!
Yes, in this case It seems to me that None
is an interesting value. I'm using file_config in a way where my application wants to know if a configuration subsection was omitted altogether and in such a case it would avoid doing the setup for that particular subsystem, which a different state than the default configuration for that subsection
Interesting. One other way of handling this would be to remove default
from the "default" kwargs for var
. Instead we would use **kwargs
. This way I can be sure whether or not the user defined a default value or not (through kwargs.pop
).
In this case I would assume that:
- If the user defines a
default
value for a nested config, then a config missing that var should use the given default value. - If the user does not define a
default
value for a nested config, then the config should be built from the empty state of the nested config type.
The cons of this approach would be:
- The function signature for
var
would no longer containdefault
(more implicit rather than explicit) - It would require you to specify
default=None
on vars you would want to come back asNone
when they don't exist in the serialized content (more explicit rather than implicit)
Looking at this, I'm kinda leaning away from removing the default
kwarg from the var
method signature. This would have to cascade down through _ConfigEntry
and in other various methods of the library...
Perhaps creating a new type UNDEFINED = typing.NewType("undefined", type(None))
would work?.
This would make the above diff closer to this...
elif is_config_type(entry.type):
if arg_key not in dictionary:
- kwargs[var.name] = arg_default
+ kwargs[var.name] = (
+ _build(entry.type, {}) if arg_default is UNDEFINED else arg_default
+ )
else:
kwargs[var.name] = _build(
entry.type, dictionary.get(arg_key, arg_default)
)
It would also change the var
method signature to this...
def var(
type=None, # noqa
- defalut=None,
+ default=UNDEFINED,
name=None,
title=None,
description=None,
required=True,
examples=None,
encoder=None,
decoder=None,
min=None, # noqa
max=None, # noqa
unique=None,
contains=None,
**kwargs,
):
Which is kinda messy (and not really Pythonic) but much easier to implement and maintain IMO.
Potential changes involving a new . I'm still unsure of the implementation as anything that doesn't define a default and is missing from the content being loaded will come through as TypeVar("undefined")
can be seen hereundefined
now (instead of None)...
I added a utils.is_undefined
to kinda keep it all together. But I'm not incredibly happy about the structure quite yet. Might take a look at doing it differently by passing the type as the default.
It seems good to me but I share your concerns about it being the ultimate default value... the first thing that comes to mind is the fact that an undefined value should not be serialized, but instead it's key should be stripped from the containing dictionary (at serialization time), for example.
I think I am going to go in a different direction honestly. I've settled to just let the default remain None
as changing that causes a lot of confusion both in development and usage of the library.
Instead I think I'll go with this logic for building nested configs...
elif is_config_type(entry.type):
if arg_key not in dictionary:
# if the default value for a nested config is the nested config class
# then build the empty state of the nested config
if is_config_type(arg_default) and entry.type == arg_default:
kwargs[var.name] = _build(entry.type, {})
else:
kwargs[var.name] = arg_default
This way, when loading nested configs, only configs that specify that the var
default is the same nested config will be loaded with the empty state of that nested config.
@file_config.config
class ParentConfig(object):
@file_config.config
class ChildConfig(object):
bar = var(str, default="Default", required=False)
foo = var(str, default="Default", required=False)
bar = var(int)
child = var(ChildConfig, default=ChildConfig, required=False)
So child
in this case would be the new way to define loading the empty state of the ChildConfig
as the default.
config_2 = ParentConfig.loads_json('{"foo": "Testing", "bar": 1}')
print(config_2)
# ParentConfig(foo='Testing', bar=1, child=ParentConfig.ChildConfig(bar='Default'))
Changes released in 0.3.7