Skip to content

Commit

Permalink
Adds a custom UnknownSettingNameError exception class (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
ababic committed Aug 26, 2018
1 parent 796d732 commit 59aba4c
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 25 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Changelog
----------------------------------

- Added the ``warn_only_if_overridden`` argument to all 'value fetching' methods on ``BaseAppSettingsHelper``, which can be used to request deprecated setting values without raising the usual 'this setting is deprecated' warning, but will raise a warning if the setting is overridden.
- Improved the consistency and usefulness of error messages raised when attribute helpers or methods are called with invalid setting names.
- Improved the consistency of error messages raised when attribute helpers or methods are called with invalid setting names, by introducing a new ``UnknownSettingNameError`` exception class and more helpful messaging.
- Renamed ``BaseAppSettingsHelper.raise_setting_error()`` to ``_raise_setting_value_error()`` (making it a private method).

0.2 (02.08.2018)
Expand Down
1 change: 1 addition & 0 deletions cogwheels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
DefaultValueFormatInvalid, DefaultValueNotImportable,
OverrideValueError, OverrideValueTypeInvalid,
OverrideValueFormatInvalid, OverrideValueNotImportable,
UnknownSettingNameError
)
from .helpers import BaseAppSettingsHelper, DeprecatedAppSetting # noqa
15 changes: 13 additions & 2 deletions cogwheels/exceptions/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from django.core.exceptions import ImproperlyConfigured

"""Errors relating to a specific setting value"""


# -----------------------------------------------------------------------------
# Common setting value errors
Expand Down Expand Up @@ -80,3 +78,16 @@ class OverrideValueFormatInvalid(SettingValueFormatInvalid, OverrideValueError):
class OverrideValueNotImportable(SettingValueNotImportable, OverrideValueError):
"""As SettingValueNotImportable, but specifically for a 'user-provided' value."""
pass


# -----------------------------------------------------------------------------
# Misc exceptions
# -----------------------------------------------------------------------------

class UnknownSettingNameError(AttributeError, ValueError):
"""A setting name used for a request does not match any settings defined
in a helper's associated defaults module. The setting name may have been
provided as a string, in which case ValueError is appropriate. But, it may
have also been referenced as a attribute, in which case AttributError is
also appropriate."""
pass
22 changes: 13 additions & 9 deletions cogwheels/helpers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
OverrideValueFormatInvalid, OverrideValueNotImportable,
DefaultValueError, DefaultValueTypeInvalid,
DefaultValueFormatInvalid, DefaultValueNotImportable,
UnknownSettingNameError
)
from cogwheels.exceptions.deprecations import (
IncorrectDeprecationsValueType, InvalidDeprecationDefinition,
Expand Down Expand Up @@ -84,8 +85,11 @@ def __getattr__(self, name):
Raises an ``AttributeError`` if the requested attribute is not a valid
setting name.
"""
if not name.isupper():
raise AttributeError("{} object has no attribute '{}'".format(
self.__class__.__name__, name))
if not self.in_defaults(name):
self._raise_invalid_setting_name_error(name, error_class=AttributeError)
self._raise_invalid_setting_name_error(name)
return self.get(name, warning_stacklevel=4)

def _set_prefix(self, init_supplied_val):
Expand Down Expand Up @@ -285,8 +289,8 @@ def is_overridden(self, setting_name):
attr_name = self.get_prefixed_setting_name(setting_name)
return hasattr(django_settings, attr_name)

def _raise_invalid_setting_name_error(self, setting_name, error_class=ValueError):
raise error_class(
def _raise_invalid_setting_name_error(self, setting_name):
raise UnknownSettingNameError(
"'{}' is not a valid setting name. Valid settings names for "
"{} are: {}." .format(
setting_name,
Expand Down Expand Up @@ -443,8 +447,7 @@ def get(self, setting_name, warn_only_if_overridden=False,
``warnings.warn()`` method, to help give a more accurate indication
of the code that caused the warning to be raised.
:type warning_stacklevel: int
:raises:
ValueError, SettingValueTypeInvalid
:raises: UnknownSettingNameError, SettingValueTypeInvalid
Instead of calling this method directly, developers are generally
encouraged to use the direct attribute shortcut, which is a
Expand Down Expand Up @@ -541,8 +544,8 @@ def get_model(self, setting_name, warn_only_if_overridden=False,
of the code that caused the warning to be raised.
:type warning_stacklevel: int
:raises:
ValueError, SettingValueTypeInvalid, SettingValueFormatInvalid,
SettingValueNotImportable
UnknownSettingNameError, SettingValueTypeInvalid,
SettingValueFormatInvalid, SettingValueNotImportable
Instead of calling this method directly, developers are generally
encouraged to use the ``models`` attribute shortcut, which is a
Expand Down Expand Up @@ -638,7 +641,8 @@ def get_module(self, setting_name, warn_only_if_overridden=False,
of the code that caused the warning to be raised.
:type warning_stacklevel: int
:raises:
ValueError, SettingValueTypeInvalid, SettingValueNotImportable
UnknownSettingNameError, SettingValueTypeInvalid,
SettingValueNotImportable
Instead of calling this method directly, developers are generally
encouraged to use the ``modules`` attribute shortcut, which is a
Expand Down Expand Up @@ -724,7 +728,7 @@ def get_object(self, setting_name, warn_only_if_overridden=False,
of the code that caused the warning to be raised.
:type warning_stacklevel: int
:raises:
ValueError, SettingValueTypeInvalid,
UnknownSettingNameError, SettingValueTypeInvalid,
SettingValueFormatInvalid, SettingValueNotImportable
Instead of calling this method directly, developers are generally
Expand Down
87 changes: 75 additions & 12 deletions cogwheels/helpers/tests/test_helper_attribute_shortcuts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
from unittest.mock import patch

from cogwheels import UnknownSettingNameError
from cogwheels.helpers import BaseAppSettingsHelper
from cogwheels.tests.base import AppSettingTestCase


class TestDirectAttributeShortcut(AppSettingTestCase):

@patch.object(BaseAppSettingsHelper, 'get')
def test_raises_unknownsettingnameerror_if_no_default_defined(self, mocked_method):
expected_message = "'UNKNOWN_SETTING' is not a valid setting name"
with self.assertRaisesRegex(UnknownSettingNameError, expected_message):
self.appsettingshelper.UNKNOWN_SETTING
mocked_method.assert_not_called()

@patch.object(BaseAppSettingsHelper, 'get')
def test_raises_simple_attributeerror_if_non_uppercase_attribute_not_found(self, mocked_method):
for attribute_name in (
'lower_case_attr',
'MIXED_case_attr'
'CamelCaseAttr'
):
try:
getattr(self.appsettingshelper, attribute_name)
except AttributeError as e:
self.assertNotIsInstance(e, UnknownSettingNameError)
mocked_method.assert_not_called()


class TestModelsShortcut(AppSettingTestCase):
"""
Each settings helper instance has a 'models' attribute, which allows
Expand All @@ -16,10 +40,23 @@ class TestModelsShortcut(AppSettingTestCase):
know there is no such setting).
"""
@patch.object(BaseAppSettingsHelper, 'get_model')
def test_raises_attributeerror_if_no_default_defined(self, mocked_method):
expected_message = "'I_DONT_THINK_SO' is not a valid setting name"
with self.assertRaisesRegex(AttributeError, expected_message):
self.appsettingshelper.models.I_DONT_THINK_SO
def test_raises_unknownsettingnameerror_if_no_default_defined(self, mocked_method):
expected_message = "'UNKNOWN_SETTING' is not a valid setting name"
with self.assertRaisesRegex(UnknownSettingNameError, expected_message):
self.appsettingshelper.models.UNKNOWN_SETTING
mocked_method.assert_not_called()

@patch.object(BaseAppSettingsHelper, 'get_model')
def test_raises_simple_attributeerror_if_non_uppercase_attribute_not_found(self, mocked_method):
for attribute_name in (
'lower_case_attr',
'MIXED_case_attr'
'CamelCaseAttr'
):
try:
getattr(self.appsettingshelper.models, attribute_name)
except AttributeError as e:
self.assertNotIsInstance(e, UnknownSettingNameError)
mocked_method.assert_not_called()

@patch.object(BaseAppSettingsHelper, 'get_model')
Expand Down Expand Up @@ -52,10 +89,23 @@ class TestModulesShortcut(AppSettingTestCase):
know there is no such setting).
"""
@patch.object(BaseAppSettingsHelper, 'get_module')
def test_raises_attributeerror_if_no_default_defined(self, mocked_method):
expected_message = "'I_DONT_THINK_SO' is not a valid setting name"
with self.assertRaisesRegex(AttributeError, expected_message):
self.appsettingshelper.modules.I_DONT_THINK_SO
def test_raises_unknownsettingnameerror_if_no_default_defined(self, mocked_method):
expected_message = "'UNKNOWN_SETTING' is not a valid setting name"
with self.assertRaisesRegex(UnknownSettingNameError, expected_message):
self.appsettingshelper.modules.UNKNOWN_SETTING
mocked_method.assert_not_called()

@patch.object(BaseAppSettingsHelper, 'get_module')
def test_raises_simple_attributeerror_if_non_uppercase_attribute_not_found(self, mocked_method):
for attribute_name in (
'lower_case_attr',
'MIXED_case_attr'
'CamelCaseAttr'
):
try:
getattr(self.appsettingshelper.modules, attribute_name)
except AttributeError as e:
self.assertNotIsInstance(e, UnknownSettingNameError)
mocked_method.assert_not_called()

@patch.object(BaseAppSettingsHelper, 'get_module')
Expand Down Expand Up @@ -87,10 +137,23 @@ class TestObjectsShortcut(AppSettingTestCase):
default value defined
"""
@patch.object(BaseAppSettingsHelper, 'get_object')
def test_raises_attributeerror_if_no_default_defined(self, mocked_method):
expected_message = "'I_DONT_THINK_SO' is not a valid setting name"
with self.assertRaisesRegex(AttributeError, expected_message):
self.appsettingshelper.objects.I_DONT_THINK_SO
def test_raises_unknownsettingnameerror_if_setting_not_in_defaults(self, mocked_method):
expected_message = "'UNKNOWN_SETTING' is not a valid setting name"
with self.assertRaisesRegex(UnknownSettingNameError, expected_message):
self.appsettingshelper.objects.UNKNOWN_SETTING
mocked_method.assert_not_called()

@patch.object(BaseAppSettingsHelper, 'get_object')
def test_raises_simple_attributeerror_if_non_uppercase_attribute_not_found(self, mocked_method):
for attribute_name in (
'lower_case_attr',
'MIXED_case_attr'
'CamelCaseAttr'
):
try:
getattr(self.appsettingshelper.objects, attribute_name)
except AttributeError as e:
self.assertNotIsInstance(e, UnknownSettingNameError)
mocked_method.assert_not_called()

@patch.object(BaseAppSettingsHelper, 'get_object')
Expand Down
5 changes: 4 additions & 1 deletion cogwheels/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ def __init__(self, settings_helper, getter_method_name):
self.getter_method_name = getter_method_name

def __getattr__(self, name):
if not name.isupper():
raise AttributeError("{} object has no attribute '{}'".format(
self.__class__.__name__, name))
if not self.settings_helper.in_defaults(name):
self.settings_helper._raise_invalid_setting_name_error(name, error_class=AttributeError)
self.settings_helper._raise_invalid_setting_name_error(name)
return self.get_value_via_helper_method(name)

def get_value_via_helper_method(self, setting_name):
Expand Down

0 comments on commit 59aba4c

Please sign in to comment.