Skip to content
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

Merged
merged 1 commit into from Oct 19, 2018
Merged

Conversation

rsnyman
Copy link
Contributor

@rsnyman rsnyman commented Sep 27, 2018

  • Add the ability to merge nested structures instead of completely overwriting them

Please let me know if this looks like an acceptable solution, and what I should change to make it better.

@@ -25,6 +25,21 @@ def dictmerge(old, new):
return new


def deep_merge(old, new):
Copy link
Member

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)

Copy link
Contributor Author

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.

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

Copy link
Member

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

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

Copy link
Contributor Author

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.

@@ -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)
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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.

@rochacbruno
Copy link
Member

When #84 is implemented, we can also support export DYNACONF_DATA="inner.deeper=1" and then have {"data": {"inner": {"existing_key": "existing_value", "deeper": 1}}}

"""
if isinstance(old, (tuple, list)):
return old + new
if isinstance(old, dict) and isinstance(new, dict):

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)

@rsnyman rsnyman force-pushed the merge-settings branch 3 times, most recently from 835402b to d7214c4 Compare September 28, 2018 22:01
@codecov
Copy link

codecov bot commented Sep 28, 2018

Codecov Report

Merging #88 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff          @@
##           master    #88   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          17     17           
  Lines         568    570    +2     
=====================================
+ Hits          568    570    +2
Impacted Files Coverage Δ
dynaconf/loaders/json_loader.py 100% <100%> (ø) ⬆️
dynaconf/loaders/toml_loader.py 100% <100%> (ø) ⬆️
dynaconf/loaders/py_loader.py 100% <100%> (ø) ⬆️
dynaconf/loaders/yaml_loader.py 100% <100%> (ø) ⬆️
dynaconf/loaders/ini_loader.py 100% <100%> (ø) ⬆️
dynaconf/utils/__init__.py 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 61c3618...458ffa6. Read the comment docs.

@rsnyman rsnyman changed the title [WIP] Merge data structures [RFR] Merge data structures Oct 2, 2018
@rsnyman
Copy link
Contributor Author

rsnyman commented Oct 4, 2018

@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,
Copy link
Member

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

?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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'

Copy link
Contributor Author

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)

@rochacbruno
Copy link
Member

@rsnyman I' ll perform some manual tests on this PR this week .

Copy link
Contributor

@sirex sirex left a 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}

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'}]
Copy link
Contributor

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
@rochacbruno rochacbruno merged commit 458ffa6 into dynaconf:master Oct 19, 2018
@rochacbruno
Copy link
Member

rochacbruno added a commit that referenced this pull request Oct 26, 2018
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants