From cdc71816ccd73b95cec868c82ace32aa31b87908 Mon Sep 17 00:00:00 2001 From: Benjamin Root Date: Thu, 11 Jul 2019 16:27:39 -0400 Subject: [PATCH 1/2] (mostly) Fix configobj copying for py3.6+ Closes #188 --- src/configobj/__init__.py | 17 ++++++- src/tests/test_configobj.py | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/configobj/__init__.py b/src/configobj/__init__.py index 928c208..61ecbad 100644 --- a/src/configobj/__init__.py +++ b/src/configobj/__init__.py @@ -433,7 +433,7 @@ def __setstate__(self, state): self.__dict__.update(state[1]) def __reduce__(self): - state = (dict(self), self.__dict__) + state = (self.copy(), self.__dict__) return (__newobj__, (self.__class__,), state) @@ -462,6 +462,21 @@ def __init__(self, parent, depth, main, indict=None, name=None): for entry, value in indict.items(): self[entry] = value + def copy(self): + """ + This will return a dictionary representation without + interpolating strings. + """ + # In py36, dict.copy() was changed + # so that it used the subclass's __getitem__, + # which causes interpolation to occur. + # See github issue #188. + orig_interp = self.main.interpolation + self.main.interpolation = False + try: + return dict.copy(self) + finally: + self.main.interpolation = orig_interp def _initialise(self): # the sequence of scalar values in this Section diff --git a/src/tests/test_configobj.py b/src/tests/test_configobj.py index c72e364..e3dd993 100644 --- a/src/tests/test_configobj.py +++ b/src/tests/test_configobj.py @@ -2,7 +2,9 @@ # pylint: disable=wildcard-import, missing-docstring, no-self-use, bad-continuation # pylint: disable=invalid-name, redefined-outer-name, too-few-public-methods from __future__ import unicode_literals +import copy import os +import sys import re import warnings @@ -176,6 +178,92 @@ def test_interoplation_repr(): repr(c) +class TestCopies(object): + @pytest.fixture + def simple_conf(self): + return ConfigObj(['foo = bar', 'baz = $foo'], interpolation='Template') + + @pytest.fixture + def nested_conf(self): + return ConfigObj(['[a]', 'foo = bar', 'baz = $foo'], + interpolation='Template') + + def test_simple_copy_method(self, simple_conf): + conf_dict = simple_conf.copy() + assert conf_dict == {'foo': 'bar', 'baz': '$foo'} + + def test_nested_copy_method(self, nested_conf): + conf_dict = nested_conf.copy() + assert conf_dict == {'a': {'foo': 'bar', 'baz': '$foo'}} + + @pytest.mark.xfail(sys.version_info >= (3, 6), + reason="dict() now uses self.items()" + " instead of dict.items()", + strict=True) + def test_simple_dict(self, simple_conf): + conf_dict = dict(simple_conf) + assert conf_dict == {'foo': 'bar', 'baz': '$foo'} + + # Not exactly sure why this one does not fail in the same + # case as test_simple_dict, but at least this is a step in + # the right direction. + def test_nested_dict(self, nested_conf): + # NOTE: this is a subtle behavior change for v5.1.0. + # previously, it would interpolate 'baz'. + conf_dict = dict(nested_conf) + assert conf_dict == {'a': {'foo': 'bar', 'baz': '$foo'}} + + def test_simple_copy(self, simple_conf): + conf_new = copy.copy(simple_conf) + assert conf_new is not simple_conf + assert isinstance(conf_new, ConfigObj) + assert conf_new.copy() == simple_conf.copy() + conf_new['foo'] = 'abc' + # Ensure the original configobj remains unchanged + assert simple_conf['foo'] == 'bar' + assert simple_conf['baz'] == 'bar' + assert conf_new['baz'] == 'abc' + assert conf_new.copy() == {'foo': 'abc', 'baz': '$foo'} + + def test_nested_copy(self, nested_conf): + conf_new = copy.copy(nested_conf) + assert conf_new is not nested_conf + assert isinstance(conf_new, ConfigObj) + assert conf_new['a'] is nested_conf['a'] + assert conf_new.copy() == nested_conf.copy() + conf_new['a']['foo'] = 'abc' + # Ensure the original configobj was changed + assert nested_conf['a']['foo'] == 'abc' + assert nested_conf['a']['baz'] == 'abc' + assert conf_new['a']['baz'] == 'abc' + assert conf_new.copy() == {'a': {'foo': 'abc', 'baz': '$foo'}} + + def test_simple_deepcopy(self, simple_conf): + conf_new = copy.deepcopy(simple_conf) + assert conf_new is not simple_conf + assert isinstance(conf_new, ConfigObj) + assert conf_new.copy() == simple_conf.copy() + conf_new['foo'] = 'abc' + # Ensure the original configobj remains unchanged + assert simple_conf['foo'] == 'bar' + assert simple_conf['baz'] == 'bar' + assert conf_new['baz'] == 'abc' + assert conf_new.copy() == {'foo': 'abc', 'baz': '$foo'} + + def test_nested_copy(self, nested_conf): + conf_new = copy.deepcopy(nested_conf) + assert conf_new is not nested_conf + assert isinstance(conf_new, ConfigObj) + assert conf_new['a'] is not nested_conf['a'] + assert conf_new.copy() == nested_conf.copy() + conf_new['a']['foo'] = 'abc' + # Ensure the original configobj remains unchanged + assert nested_conf['a']['foo'] == 'bar' + assert nested_conf['a']['baz'] == 'bar' + assert conf_new['a']['baz'] == 'abc' + assert conf_new.copy() == {'a': {'foo': 'abc', 'baz': '$foo'}} + + class TestEncoding(object): @pytest.fixture def ant_cfg(self): From 451976b309c39d319e828cddeb20940a6c8c1bcf Mon Sep 17 00:00:00 2001 From: Benjamin Root Date: Thu, 11 Jul 2019 16:45:39 -0400 Subject: [PATCH 2/2] Be specific about exactly which minor version of py3.6 broke things --- src/tests/test_configobj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_configobj.py b/src/tests/test_configobj.py index e3dd993..ae5a0c9 100644 --- a/src/tests/test_configobj.py +++ b/src/tests/test_configobj.py @@ -196,7 +196,7 @@ def test_nested_copy_method(self, nested_conf): conf_dict = nested_conf.copy() assert conf_dict == {'a': {'foo': 'bar', 'baz': '$foo'}} - @pytest.mark.xfail(sys.version_info >= (3, 6), + @pytest.mark.xfail(sys.version_info >= (3, 6, 7), reason="dict() now uses self.items()" " instead of dict.items()", strict=True)