From 3ae971468ff3a6712aa47ad3a5d2b9f0c9663b60 Mon Sep 17 00:00:00 2001 From: Julia Medina Date: Tue, 29 Jul 2014 18:47:49 -0300 Subject: [PATCH] Add Settings.copy, freeze and frozencopy method --- docs/topics/api.rst | 24 ++++++++++++++++++++++ scrapy/settings/__init__.py | 16 +++++++++++++++ tests/test_settings/__init__.py | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 341340c2a..16bfe5f8f 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -264,6 +264,30 @@ Settings API :param default: the value to return if no setting is found :type default: any + .. method:: copy() + + Make a deep copy of current settings. + + This method returns a new instance of the :class:`Settings` class, + populated with the same values and their priorities. + + Modifications to the new object won't be reflected on the original + settings. + + .. method:: freeze() + + Disable further changes to the current settings. + + After calling this method, the present state of the settings will become + immutable. Trying to change values through the :meth:`~set` method and + its variants won't be possible and will be alerted. + + .. method:: frozencopy() + + Return an immutable copy of the current settings. + + Alias for a :meth:`~freeze` call in the object returned by :meth:`copy` + .. _topics-api-signals: Signals API diff --git a/scrapy/settings/__init__.py b/scrapy/settings/__init__.py index 2dd6a2920..978174694 100644 --- a/scrapy/settings/__init__.py +++ b/scrapy/settings/__init__.py @@ -1,5 +1,6 @@ import six import json +import copy import warnings from collections import MutableMapping from importlib import import_module @@ -46,6 +47,7 @@ class SettingsAttribute(object): class Settings(object): def __init__(self, values=None, priority='project'): + self.frozen = False self.attributes = {} self.setmodule(default_settings, priority='default') if values is not None: @@ -93,6 +95,7 @@ class Settings(object): raise ValueError("Cannot convert value for setting '%s' to dict: '%s'" % (name, value)) def set(self, name, value, priority='project'): + assert not self.frozen, "Trying to modify an immutable Settings object" if isinstance(priority, six.string_types): priority = SETTINGS_PRIORITIES[priority] if name not in self.attributes: @@ -101,16 +104,29 @@ class Settings(object): self.attributes[name].set(value, priority) def setdict(self, values, priority='project'): + assert not self.frozen, "Trying to modify an immutable Settings object" for name, value in six.iteritems(values): self.set(name, value, priority) def setmodule(self, module, priority='project'): + assert not self.frozen, "Trying to modify an immutable Settings object" if isinstance(module, six.string_types): module = import_module(module) for key in dir(module): if key.isupper(): self.set(key, getattr(module, key), priority) + def copy(self): + return copy.deepcopy(self) + + def freeze(self): + self.frozen = True + + def frozencopy(self): + copy = self.copy() + copy.freeze() + return copy + @property def overrides(self): warnings.warn("`Settings.overrides` attribute is deprecated and won't " diff --git a/tests/test_settings/__init__.py b/tests/test_settings/__init__.py index 39e47dec6..c7e0914d6 100644 --- a/tests/test_settings/__init__.py +++ b/tests/test_settings/__init__.py @@ -190,6 +190,42 @@ class SettingsTest(unittest.TestCase): self.assertEqual(settings.getdict('TEST_DICT3', {'key1': 5}), {'key1': 5}) self.assertRaises(ValueError, settings.getdict, 'TEST_LIST1') + def test_copy(self): + values = { + 'TEST_BOOL': True, + 'TEST_LIST': ['one', 'two'], + 'TEST_LIST_OF_LISTS': [['first_one', 'first_two'], + ['second_one', 'second_two']] + } + self.settings.setdict(values) + copy = self.settings.copy() + self.settings.set('TEST_BOOL', False) + self.assertTrue(copy.get('TEST_BOOL')) + + test_list = self.settings.get('TEST_LIST') + test_list.append('three') + self.assertListEqual(copy.get('TEST_LIST'), ['one', 'two']) + + test_list_of_lists = self.settings.get('TEST_LIST_OF_LISTS') + test_list_of_lists[0].append('first_three') + self.assertListEqual(copy.get('TEST_LIST_OF_LISTS')[0], + ['first_one', 'first_two']) + + def test_freeze(self): + self.settings.freeze() + with self.assertRaises(AssertionError) as cm: + self.settings.set('TEST_BOOL', False) + self.assertEqual(str(cm.exception), + "Trying to modify an immutable Settings object") + + def test_frozencopy(self): + with mock.patch.object(self.settings, 'copy') as mock_copy: + with mock.patch.object(mock_copy, 'freeze') as mock_freeze: + mock_object = self.settings.frozencopy() + mock_copy.assert_call_once() + mock_freeze.assert_call_once() + self.assertEqual(mock_object, mock_copy.return_value) + def test_deprecated_attribute_overrides(self): self.settings.set('BAR', 'fuz', priority='cmdline') with warnings.catch_warnings(record=True) as w: