New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFR] Merge data structures #88
Conversation
dynaconf/utils/__init__.py
Outdated
@@ -25,6 +25,21 @@ def dictmerge(old, new): | |||
return new | |||
|
|||
|
|||
def deep_merge(old, new): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is already this function https://github.com/rochacbruno/dynaconf/blob/master/dynaconf/utils/__init__.py#L15 dictmerge
which is used by dynaconf cli write
commands. I think the purposes are the same. Maybe we can keep only one function for this and extend its functionality. (you implementation looks more complete)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was thinking of doing that. I tried dictmerge initially, but it only handles dicts. I'll replace dictmerge with my function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should be clearly defined if this should be a deep update or a merged copy, one shouldn't have a return value, the other shouldn't affect its arguments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RonnyPfannschmidt good point! in_place
argument can control it. @rsnyman
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i would strongly advise towards picking one implementation behavior and then sticking to it
i have a gut feeling that choice in this dimension is going to be more expensive than desirable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RonnyPfannschmidt Good point. An in-place merge is probably better.
dynaconf/default_settings.py
Outdated
@@ -85,6 +85,9 @@ def start_dotenv(obj=None): | |||
# The default encoding to open settings files | |||
ENCODING_FOR_DYNACONF = get('ENCODING_FOR_DYNACONF', 'utf-8') | |||
|
|||
# Merge dictionaries on load | |||
MERGE_CONF_FOR_DYNACONF = get('MERGE_CONF_FOR_DYNACONF', False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like having this feature as optional, and this config enables it globally, what about cases where we need only specific data to be merged and others to be overwritten.
Is it a good idea to have a kind of super()
keyword (e.g: _merge, or dynaconf_merge) to give a fine control over where the merge is being applied.
Example.
settings.yml
default:
data:
one: 1
two: 2
Now if I want to add a three
I can
othersettings.yaml
default:
_dynaconf_merge: true
data:
three: 3
or maybe
yeatanothersettings.yaml
default:
data:
_dynaconf_merge: true
four: 4
and at the end I have
>> settings.DATA
{
'one': 1,
'two': 2,
'three': 3,
'four': 4,
}
The same can be applied to environment variables
$ export DYNACONF_DATA="{five = 5, six = 6, dynaconf_merge = true}"
If I dont supply the special key dynaconf_merge
(we can name it just _merge
), then the cascade will overwrite the values.
What do you think is best?
- Fine control
- global set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of fine control, but I'm not sure that another key in the settings file is necessarily the correct way to do it. It looks like a setting here, but when you actually look at your settings, there's no key.
Perhaps a better approach would be to start with a global setting, and then think about a good way to implement the fine control.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rsnyman agreed! the fine control doesn't block the global implementation.
If we are going to add special keywords then we need to think about the naming and test if all the file formats supports names like _dynaconf_merge
, _dynaconf_include
etc.
When #84 is implemented, we can also support |
""" | ||
if isinstance(old, (tuple, list)): | ||
return old + new | ||
if isinstance(old, dict) and isinstance(new, dict): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to keep track of where a value is from (imagine wanting to write current updates/changes to a configfile while ignoring all inherited data)
835402b
to
d7214c4
Compare
Codecov Report
@@ Coverage Diff @@
## master #88 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 17 17
Lines 568 570 +2
=====================================
+ Hits 568 570 +2
Continue to review full report at Codecov.
|
@rochacbruno Is there anything else you want me to do with this? |
dynaconf/base.py
Outdated
@@ -494,10 +494,17 @@ def set(self, key, value, loader_identifier=None, tomlfy=False): | |||
:param tomlfy: Bool define if value is parsed by toml (defaults False) | |||
""" | |||
value = parse_conf_data(value, tomlfy=tomlfy) | |||
key = key.strip().upper() | |||
|
|||
if 'FOR_DYNACONF' not in key and getattr(self, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand this condition, can you explain the case where FOR_DYNACONF
key is set and should not be merged.
Also about the name of the variable MERGE_CONF_FOR_DYNACONF
should we adopt something different?
ENABLE_MERGE_FOR_DYNACONF
MERGE_ENABLED_FOR_DYNACONF
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was experiencing some issues with keys, I'll test it again and see if it makes a difference.
As for the name, I don't mind either of those, I would just recommend they follow the same convention as the rest of the names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I remove the check for DynaConf variables, I get the following traceback:
Traceback (most recent call last):
...
File "dynaconf/base.py", line 500, in set
attr = self.get(key, default=None)
File "dynaconf/base.py", line 231, in get
(fresh or self._fresh or key in self.FRESH_VARS_FOR_DYNACONF) and
AttributeError: 'Settings' object has no attribute 'FRESH_VARS_FOR_DYNACONF'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having said that, if I change line 500 in base.py
to use getattr
instead, it works.
attr = getattr(self, key, None)
d7214c4
to
e2b3dd3
Compare
@rsnyman I' ll perform some manual tests on this PR this week . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just tested it manually with settings.toml
:
[default]
foo.bar = 1
foo.baz = 2
[testing]
foo.bar = 3
And it works:
from dynaconf import LazySettings
settings = LazySettings(MERGE_ENABLED_FOR_DYNACONF=True)
settings.setenv('testing')
print(settings.FOO)
Output:
{'bar': 3, 'baz': 2}
tests/test_base.py
Outdated
settings.set('MERGE_KEY', [{'name': 'item 1'}, {'name': 'item 2'}]) | ||
settings.set('MERGE_KEY', [{'name': 'item 3'}, {'name': 'item 4'}]) | ||
assert settings.MERGE_KEY == [{'name': 'item 1'}, {'name': 'item 2'}, | ||
{'name': 'item 3'}, {'name': 'item 4'}] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks, that this only tests list case, but not nested dicts. Probably it would be useful to also tests nested dicts.
- Addd the ability to merge nested structures instead of completely overwriting them - Use monkeypatch to stop one test from interferring with another - Updated documentation
e2b3dd3
to
458ffa6
Compare
Merged and added examples https://github.com/rochacbruno/dynaconf/tree/master/example/merge_configs |
- Added `MERGE_ENABLED_FOR_DYNACONF` with ability to merge nested dictionaries instead of replacing PR #88 - Support for dot notation to access nested dictionaries like `settings['KEY.OTHER.NESTED.DEEPER']` PR #93 - Support dotted notation for validators PR #98 - Fixed a bug in SETTINGS_MODULE cleanup when `.setenv` method was called PR #97 - Added Python 3.7 to test matrix PR #99
Please let me know if this looks like an acceptable solution, and what I should change to make it better.