Skip to content

Commit

Permalink
Simplify BaseAppSettingsHelper.__init__() (#14)
Browse files Browse the repository at this point in the history
Simplifying BaseAppSettingsHelper.__init__():

- Remed support for 'prefix', 'defaults_path' and 'deprecations' to be specified at initialisation
- Simplified _set_prefix()
- Updated _load_defaults() to set self._defaults_module_path itself and remove _set_defaults_module_path()
- Renamed perepare_deprecation_data() to prepare_deprecation_data(), and reference the 'deprecations' class attribute directly, instead of setting and reading from a '_deprecations' attribute

DeprecatedAppSetting changes:

- In deprecation warning message formats, use '{removing_in_version}' instead of '{removed_in_version}' and use '{prefixed_setting_name}' and {prefixed_replacement_name}' instead of joining {prefix}, {setting_name} and {replacement_name} throughout
- Renamed get_removed_in_version_text() to get_removing_in_version_text()
- Added prefixed_setting_name() and prefixed_replacement_name() property methods
- Updated BaseAppSettingsHelper._prepare_deprecation_data() to use get_prefix() when setting the prefix value for each DeprecatedAppSetting instance
  • Loading branch information
ababic committed Aug 26, 2018
1 parent 59aba4c commit bab6ee1
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 124 deletions.
32 changes: 22 additions & 10 deletions cogwheels/helpers/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
COMMON_REQUESTED_WARNING_FORMAT = (
"Please update your code to reference the new setting, as continuing to "
"reference {setting_name} will cause an exception to be raised once support "
"is removed in {removed_in_version}."
"is removed in {removing_in_version}."
)
RENAMED_SETTING_REQUESTED_WARNING_FORMAT = (
"The {setting_name} app setting has been renamed to {replacement_name}. "
Expand All @@ -19,29 +19,29 @@
"The {setting_name} app setting is deprecated. Please remove any "
"references to it from your project, as continuing to reference it will "
"cause an exception to be raised once support is removed in "
"{removed_in_version}."
"{removing_in_version}."
)

DEPRECATED_SETTING_OVERRIDDEN_WARNING_FORMAT = (
"The {prefix}_{setting_name} setting is deprecated. The override value "
"from your project's Django settings will no longer have any affect "
"once support is removed in {removed_in_version}."
"once support is removed in {removing_in_version}."
)

COMMON_OLD_SETTING_USED_WARNING_FORMAT = (
"Please update your Django settings to use the new setting, otherwise the "
"app will revert to it's default behaviour once support for "
"{prefix}_{setting_name} is removed in {removed_in_version}."
"{prefixed_setting_name} is removed in {removing_in_version}."
)

RENAMED_OLD_SETTING_USED_WARNING_FORMAT = (
"The {prefix}_{setting_name} setting has been renamed to "
"{prefix}_{replacement_name}. "
"The {prefixed_setting_name} setting has been renamed to "
"{prefixed_replacement_name}. "
) + COMMON_OLD_SETTING_USED_WARNING_FORMAT

REPLACED_OLD_SETTING_USER_WARNING_FORMAT = (
"The {prefix}_{setting_name} setting is deprecated in favour of using "
"{prefix}_{replacement_name}. "
"The {prefixed_setting_name} setting is deprecated in favour of using "
"{prefixed_replacement_name}. "
) + COMMON_OLD_SETTING_USED_WARNING_FORMAT


Expand Down Expand Up @@ -76,7 +76,17 @@ def prefix(self):
def prefix(self, value):
self._prefix = value

def get_removed_in_version_text(self):
@property
def prefixed_setting_name(self):
return self.prefix + self.setting_name

@property
def prefixed_replacement_name(self):
if self.replacement_name is None:
return ''
return self.prefix + self.replacement_name

def get_removing_in_version_text(self):
# To be removed once 'removed_in' is required.
if self.removing_in is not None:
return self.removing_in
Expand All @@ -91,7 +101,9 @@ def _make_warning_message(self, message_format):
prefix=self.prefix,
setting_name=self.setting_name,
replacement_name=self.replacement_name,
removed_in_version=self.get_removed_in_version_text(),
prefixed_setting_name=self.prefixed_setting_name,
prefixed_replacement_name=self.prefixed_replacement_name,
removing_in_version=self.get_removing_in_version_text(),
)

def warn_if_overridden(self, stacklevel=2):
Expand Down
78 changes: 24 additions & 54 deletions cogwheels/helpers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,15 @@ class BaseAppSettingsHelper:
defaults_path = None
deprecations = ()

def __init__(self, prefix=None, defaults_path=None, deprecations=None):
def __init__(self):
self.__module_path_split = self.__class__.__module__.split('.')
self._set_prefix(prefix)
self._set_prefix()

# Load values from defaults module
self._set_defaults_module_path(defaults_path)
self._load_defaults()

# Load deprecation data
if deprecations is not None:
self._deprecations = deprecations
else:
self._deprecations = self.__class__.deprecations
self._perepare_deprecation_data()
self._prepare_deprecation_data()

# This will create the dictionaries if they don't already exist
self.reset_caches()
Expand Down Expand Up @@ -92,7 +87,7 @@ def __getattr__(self, name):
self._raise_invalid_setting_name_error(name)
return self.get(name, warning_stacklevel=4)

def _set_prefix(self, init_supplied_val):
def _set_prefix(self):
"""
Called by ``__init()__`` to set the object's ``_prefix`` attribute,
which determines the prefix app users must use when overriding
Expand All @@ -117,49 +112,15 @@ def _set_prefix(self, init_supplied_val):
A helper class is defined in ``yourapp/subapp/conf/settings.py`` or
``yourapp/subapp/settings.py`` would be assigned the prefix: ``"YOURAPP_SUBAPP"``.
"""
if init_supplied_val is not None:
value = init_supplied_val.rstrip('_')
elif self.__class__.prefix is not None:
value = self.__class__.prefix.rstrip('_')
if self.prefix is not None:
value = self.prefix.rstrip('_')
else:
module_path_parts = self.__module_path_split[:-1]
try:
module_path_parts.remove('conf')
except ValueError:
pass
if module_path_parts[-1] == 'conf':
module_path_parts.pop()
value = '_'.join(module_path_parts)
self._prefix = value.upper()

def _set_defaults_module_path(self, init_supplied_val):
"""
Called by ``__init__()`` to set the object's ``_defaults_module_path``
attribute, which should be a valid import path string for the
``defaults`` module linked to this helper.
Developers can specify the path by setting the ``defaults_path``
attribute on their helper class (most likely), or using the
``defaults_path`` argument when initialising the helper instance (
should only really be used for testing purposes).
If no value is specified, a deterministic default value is generated,
based on where the helper class is defined. It is assumed that the
defaults module is defined in the same directory as the settings helper
class. For example:
If the settings helper is defined in ``yourapp/config/settings.py``,
the defaults module path is assumed to be ``yourapp/config/defaults.py``.
If the settings helper is defined in ``yourapp/some_other_directory/settings.py``,
the defaults module path is assumed to be ``yourapp/some_other_directory/defaults.py``.
"""
if init_supplied_val is not None:
value = init_supplied_val
elif self.__class__.defaults_path is not None:
value = self.__class__.defaults_path
else:
value = '.'.join(self.__module_path_split[:-1]) + ".defaults"
self._defaults_module_path = value

@staticmethod
def _do_import(module_path):
"""A simple wrapper for importlib.import_module()."""
Expand All @@ -174,21 +135,30 @@ def _make_cache_key(setting_name, accept_deprecated):

def _load_defaults(self):
"""
Called by ``__init__()`` to generate a dictionary of the relevant
Called by ``__init__()`` to create a dictionary of the relevant
values from the associated defaults module, and save it to the
object's ``_defaults`` attribute to improve lookup performance.
Only variables with upper-case names are included.
It is assumed that the defaults module is defined in the same directory
as ``settings.py`` where the settings helper class is defined. But,
in cases where this differs, developers can specify an alternative
import path using the ``defaults_path`` class attribute for their
helper class.
"""
self._defaults_module_path = self.defaults_path or \
'.'.join(self.__module_path_split[:-1]) + ".defaults"

module = self._do_import(self._defaults_module_path)
self._defaults = {
k: v for k, v in module.__dict__.items()
if k.isupper()
}

def _perepare_deprecation_data(self):
def _prepare_deprecation_data(self):
"""
Cycles through the list of AppSettingDeprecation instances set on
``self._deprecations`` and propulates two new dictionary attributes:
``self.deprecations`` and prepulates two new dictionary attributes:
``self._deprecated_settings``:
Uses the deprecated setting name as the keys, and used to
Expand All @@ -199,17 +169,17 @@ def _perepare_deprecation_data(self):
allows us to temporarily support user-defined settings using the
old name when the values for the new setting are requested.
"""
if not isinstance(self._deprecations, (list, tuple)):
if not isinstance(self.deprecations, (list, tuple)):
raise IncorrectDeprecationsValueType(
"'deprecations' must be a list or tuple, not a {}."
.format(type(self._deprecations).__name__)
.format(type(self.deprecations).__name__)
)

self._deprecated_settings = {}
self._replacement_settings = defaultdict(list)

for item in self._deprecations:
item.prefix = self._prefix
for item in self.deprecations:
item.prefix = self.get_prefix()

if not self.in_defaults(item.setting_name):
raise InvalidDeprecationDefinition(
Expand Down
62 changes: 22 additions & 40 deletions cogwheels/helpers/tests/test_helper_init.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,34 @@
from django.test import TestCase

from cogwheels.helpers import BaseAppSettingsHelper, DeprecatedAppSetting
from cogwheels import BaseAppSettingsHelper


class TestSettingsHelper(BaseAppSettingsHelper):
defaults_path = 'cogwheels.tests.conf.defaults'
prefix = 'TEST_'
deprecations = ()
pass


class TestHelperInit(TestCase):
class TestSettingsHelperInit(TestCase):

def test_providing_prefix_overrides_the_class_attribute_value(self):
test_val = 'ABRACADABRA'
self.assertEqual(
TestSettingsHelper(prefix=test_val)._prefix,
test_val
)
def setUp(self):
# Reset TestSettingsHelper class attributes before each test
TestSettingsHelper.defaults_path = 'cogwheels.tests.conf.defaults'
TestSettingsHelper.prefix = None
TestSettingsHelper.deprecations = ()

def test_prefix_value_is_converted_to_uppercase(self):
def test_set_prefix_converts_specified_value_to_uppercase(self):
lowercase_prefix = 'beep'
uppercase_prefix = 'BEEP'
self.assertEqual(
TestSettingsHelper(prefix=lowercase_prefix)._prefix,
uppercase_prefix
)

def test_prefix_attribute_never_has_trailing_underscores(self):
self.assertEqual(TestSettingsHelper()._prefix, 'TEST')
self.assertEqual(TestSettingsHelper(prefix="ABRACADABRA_")._prefix, 'ABRACADABRA')
self.assertEqual(TestSettingsHelper(prefix="BEEP_BOOP___")._prefix, 'BEEP_BOOP')

def test_providing_defaults_path_overrides_the_class_attribute_value(self):
test_val = 'cogwheels'
self.assertIs(
TestSettingsHelper(defaults_path=test_val)._defaults_module_path,
test_val
)

def test_providing_deprecations_overrides_the_class_attribute_value(self):
test_val = (
DeprecatedAppSetting('STRING_SETTING'),
)
self.assertIs(
TestSettingsHelper(deprecations=test_val)._deprecations,
test_val
)

def test_raises_import_error_if_defaults_module_cannot_be_imported(self):

TestSettingsHelper.prefix = lowercase_prefix
obj = TestSettingsHelper()
self.assertEqual(obj._prefix, uppercase_prefix)

def test_set_prefix_strips_trailing_underscores_from_specified_value(self):
TestSettingsHelper.prefix = 'TEST___'
obj = TestSettingsHelper()
self.assertEqual(obj._prefix, 'TEST')

def test_importerror_raised_if_defaults_module_does_not_exist(self):
TestSettingsHelper.defaults_path = 'invalid.module.path'
with self.assertRaises(ImportError):
TestSettingsHelper(defaults_path='invalid.module.path')
TestSettingsHelper()
51 changes: 31 additions & 20 deletions cogwheels/helpers/tests/test_invalid_deprecations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from django.test import TestCase

from cogwheels import exceptions
from cogwheels.helpers import BaseAppSettingsHelper, DeprecatedAppSetting
from cogwheels import BaseAppSettingsHelper, DeprecatedAppSetting
from cogwheels.exceptions import (
IncorrectDeprecationsValueType, InvalidDeprecationDefinition,
DuplicateDeprecationError
)


class TestSettingsHelper(BaseAppSettingsHelper):
Expand All @@ -12,27 +15,35 @@ class TestSettingsHelper(BaseAppSettingsHelper):

class TestHelperInitErrors(TestCase):

def test_raises_correct_error_type_if_deprecations_value_is_wrong_type(self):
with self.assertRaises(exceptions.IncorrectDeprecationsValueType):
TestSettingsHelper(deprecations={})
def setUp(self):
# Reset TestSettingsHelper class attributes before each test
TestSettingsHelper.deprecations = ()

def test_raises_incorrectdeprecationsvaluetype_if_deprecations_value_is_wrong_type(self):
TestSettingsHelper.deprecations = {}
with self.assertRaises(IncorrectDeprecationsValueType):
TestSettingsHelper()

def test_raises_correct_error_type_if_deprecated_value_not_found_in_defaults(self):
with self.assertRaises(exceptions.InvalidDeprecationDefinition):
TestSettingsHelper(deprecations=(
DeprecatedAppSetting('NON_EXISTENT_SETTING'),
))
TestSettingsHelper.deprecations = (
DeprecatedAppSetting('NON_EXISTENT_SETTING'),
)
with self.assertRaises(InvalidDeprecationDefinition):
TestSettingsHelper()

def test_raises_correct_error_type_if_replacement_value_not_found_in_defaults(self):
with self.assertRaises(exceptions.InvalidDeprecationDefinition):
TestSettingsHelper(deprecations=(
DeprecatedAppSetting(
'DEPRECATED_SETTING', renamed_to="NON_EXISTENT_SETTING"
),
))
TestSettingsHelper.deprecations = (
DeprecatedAppSetting(
'DEPRECATED_SETTING', renamed_to="NON_EXISTENT_SETTING"
),
)
with self.assertRaises(InvalidDeprecationDefinition):
TestSettingsHelper()

def test_raises_correct_error_type_if_setting_name_repeated_in_deprecation_definitions(self):
with self.assertRaises(exceptions.DuplicateDeprecationError):
TestSettingsHelper(deprecations=(
DeprecatedAppSetting('DEPRECATED_SETTING'),
DeprecatedAppSetting('DEPRECATED_SETTING'),
))
TestSettingsHelper.deprecations = (
DeprecatedAppSetting('DEPRECATED_SETTING'),
DeprecatedAppSetting('DEPRECATED_SETTING'),
)
with self.assertRaises(DuplicateDeprecationError):
TestSettingsHelper()

0 comments on commit bab6ee1

Please sign in to comment.