Skip to content

Commit

Permalink
added 'decoupled' kwparam to merge() (closes #115)
Browse files Browse the repository at this point in the history
  • Loading branch information
jhermann committed Jul 27, 2016
1 parent 841f4c2 commit a3516a8
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 2 deletions.
12 changes: 10 additions & 2 deletions src/configobj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import os
import re
import sys
import copy
import collections

from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE
Expand Down Expand Up @@ -729,10 +730,15 @@ def dict(self):
return newdict


def merge(self, indict):
def merge(self, indict, decoupled=False):
"""
A recursive update - useful for merging config files.
Note: if ``decoupled`` is ``True``, then the target object (self)
gets its own copy of any mutable objects in the source dictionary
(both sections and values), paid for by more work for ``merge()``
and more memory usage.
>>> a = '''[section1]
... option1 = True
... [[subsection]]
Expand All @@ -749,9 +755,11 @@ def merge(self, indict):
ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}})
"""
for key, val in list(indict.items()):
if decoupled:
val = copy.deepcopy(val)
if (key in self and isinstance(self[key], collections.Mapping) and
isinstance(val, collections.Mapping)):
self[key].merge(val)
self[key].merge(val, decoupled=decoupled)
else:
self[key] = val

Expand Down
12 changes: 12 additions & 0 deletions src/tests/test_configobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,18 @@ def test_interpolation_using_default_sections():
assert c.write() == ['a = %(a)s', '[DEFAULT]', 'a = fish']


class TestMerging(object):

@pytest.mark.parametrize('data', ((False, 42), (True, '3')))
def test_merge_coupling(self, data):
c1 = ConfigObj("foo = 1,2|[sect]|val=3".split('|'))
c2 = ConfigObj("bar = 3".split('|'))
c2.merge(c1, decoupled=data[0])
assert c2['sect']['val'] == '3'
c1['sect']['val'] = 42
assert c2['sect']['val'] == data[1]


class TestErrorReporting(object):
MULTI_ERROR = [
'[]',
Expand Down

1 comment on commit a3516a8

@EliAndrewC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Couple of notes:

  1. Suppose we call conf.merge({'a': {'b': {'c': {'d': 'e'}}}}, decoupled=True). We'll end up calling copy.deepcopy on {'b': {'c': {'d': 'e'}}}, then again on {'c': {'d': 'e'}}, then again on {'d': 'e'}. Since the docstring explicitly says that it's "paid for by more work... and more memory usage" then I think this inefficiency is probably okay, especially since most of the extra memory from the extra copies will get garbage collected.

  2. TestMerging isn't necessarily testing that the deep copying aspect of the merge is functioning. To test that we'd need to try doing something like

conf = ConfigObj('x = y')
d = {'a': {'b': {'c': {'d': 'e'}}}}
conf.merge(d, decoupled=True)
assert conf['a'] is not d['a]

Please sign in to comment.