Usage
=====

Declaring your settings
-----------------------

To declare your application settings, create a settings class inheriting from
``appsettings.AppSettings``:

.. code:: python

    import appsettings


    class MySettings(appsettings.AppSettings):
        boolean_setting = appsettings.BooleanSetting(default=False)
        required_setting = appsettings.StringSetting(required=True)
        named_setting = appsettings.IntegerSetting(name='integer_setting')
        prefixed_setting = appsettings.ListSetting(prefix='my_app_')

        class Meta:
            setting_prefix = 'app_'

In this example, we declared four different settings in our class:
``boolean_setting``, ``required_setting``, ``named_setting``, and
``prefixed_setting``.

The corresponding variable names in a Django project's settings file will be:

- ``boolean_setting``: ``APP_BOOLEAN_SETTING``, because no name was given.
- ``required_setting``: ``APP_REQUIRED_SETTING``, because no name was given.
- ``named_setting``: ``APP_INTEGER_SETTING``, because a name was given.
- ``prefixed_setting``: ``MY_APP_PREFIXED_SETTING``, because we overrode the class prefix.

We could as well give both name and prefix to customize entirely the corresponding
variable name in the settings file.

Using a callable as default value
'''''''''''''''''''''''''''''''''

Sometimes you may want to pass a callable as a default value. To ensure that the
callable is called, or to prevent it, use the ``call_default`` parameter:

.. code:: python

    from datetime import datetime
    import appsettings


    class MySettings(appsettings.AppSettings):
        # expect a datetime value, allow calling
        first_access = appsettings.Setting(default=datetime.now, call_default=True)

        # expect a function returning current time, prevent calling
        now_function = appsettings.Setting(default=datetime.now, call_default=False)


``call_default`` is True by default on every setting class except ``ObjectSetting``.

.. important::

    Note that ``call_default`` is only used when the related setting is missing
    from the project settings!

Checking the settings
---------------------

The best place to check your application settings is in your
application configuration class:

.. code:: python

    import django
    import appsettings


    class AppSettings(appsettings.AppSettings):
        string_list = appsettings.ListSetting(item_type=str,
                                              empty=False,
                                              required=True,
                                              max_length=4)

        class Meta:
            setting_prefix = 'my_app_'


    class AppConfig(django.apps.AppConfig):
        name = 'my_app'
        verbose_name = 'My Application'

        def ready(self):
            # check every settings at startup, raise one exception
            # with all errors as one message
            AppSettings.check()

In the above example, if ``MY_APP_STRING_LIST`` is not defined, or if it is not
a list object, or if it's empty, or if it has more than 4 elements,
``AppSettings.check()`` will raise a ``ImproperlyConfigured`` exception.
If you had more settings declared in your settings class, then the
``ImproperlyConfigured`` exception would be raised with a message being a
concatenation of the first exception for each setting checked.

You can also check each setting individually, for example:

.. code:: python

    for setting in AppSettings.settings.values():
        setting.check()

If the setting's value is invalid, it will raise an exception
(usually ``ValueError``).

Using the settings in your code
-------------------------------

Once your settings class is ready, you will be able to instantiate it to
benefit from its simplicity of use and its caching feature:

.. code:: python

    # let say you declared your Settings class in apps.py
    from .apps import Settings

    settings = Settings()

    print(settings.string_list[0])
    print(settings.now_function())
    print(settings.first_access.day)

Testing the settings
--------------------

When you instantiate your settings class with ``settings = Settings()``,
the ``invalidate_cache`` method of the instance is automatically connected
to the ``setting_changed`` signal sent by Django. It means that you can test
different values for your settings without worrying about invalidating the
cache each time.

.. code:: python

    from django.test import SimpleTestCase, override_settings
    from my_app.apps import Settings


    class MainTestCase(TestCase):
        def setUp(self):
            self.settings = Settings()

        def test_some_settings(self):
            # first fetch
            assert self.settings.string_list[0] == 'hello'

            # django will send setting_changed signal, cache will be cleaned
            with override_settings(MY_APP_STRING_LIST=['hello world!']):
                assert len(self.settings.string_list) == 1

            # signal sent again
            with override_settings(MY_APP_STRING_LIST=['good morning', 'world', '!']):
                assert len(self.settings.string_list) == 3

            # signal is also sent when with clause ends
            assert self.settings.string_list[0] == 'hello'

        # it works the same way with decorator
        @override_settings(MY_APP_STRING_LIST=['bye'])
        def test_string_list(self):
            assert 'bye' in self.settings.string_list

Writing your own setting class
------------------------------

At some point you may want to have more complex settings. You can customize
how the setting is checked, but also how the value is transformed before being
returned.

The first way to customize the check method is to pass a callable
in the ``checker`` parameter of the setting. This callable must accepts
two parameters: name and value.

.. code:: python

    import re
    import appsettings

    def regex_checker(name, value):
        re_type = type(re.compile(r'^$'))
        if not isinstance(value, (re_type, str)):
            # raise whatever exception
            raise ValueError('%s must be a a string or a compiled regex '
                             '(use re.compile)' % name)


    setting = appsettings.Setting(checker=regex_checker)

.. important::

    Note that only the ``appsettings.Setting`` class accepts the ``checker``
    parameter! Other subclasses like ``appsettings.PositiveIntegerSetting``
    already have a custom checker and therefore do not allow to change it.

The second way is to subclass ``appsettings.Setting`` and write a custom
``checker`` method:

.. code:: python

    import re
    import appsettings


    class RegexSetting(appsettings.Setting):
        def checker(name, value):
            re_type = type(re.compile(r'^$'))
            if not isinstance(value, (re_type, str)):
                # raise whatever exception
                raise ValueError('%s must be a a string or a compiled regex '
                                 '(use re.compile)' % name)


    setting = RegexSetting()

Writing your own type checker
'''''''''''''''''''''''''''''

The third way to customize how the setting is checked is to create
a new ``TypeChecker`` class:

.. code:: python

    import re
    import appsettings


    # option 1: passing a specific base_type (can be a tuple with several types)
    class RegexTypeChecker1(appsettings.TypeChecker):
        def __init__(self):
            re_type = type(re.compile(r'^$'))
            super(BooleanTypeChecker, self).__init__(base_type=(str, re_type))


    setting1 = appsettings.Setting(checker=RegexTypeChecker1())


    # option 2: completely overriding the __call__ method
    class RegexTypeChecker2(appsettings.TypeChecker):
        def __call__(self, name, value):
            re_type = type(re.compile(r'^$'))
            if not isinstance(value, (re_type, str)):
                # raise whatever exception
                raise ValueError('%s must be a a string or a compiled regex '
                                 '(use re.compile)' % name)


    setting2 = appsettings.Setting(checker=RegexTypeChecker2())


    # option 3: combining both type checker and setting class
    class RegexSetting(appsettings.Setting):
        def __init__(
                self, name='', default=re.compile(r'^$'), required=False,
                prefix='', call_default=True, transform_default=False):
            super(RegexSetting, self).__init__(
                name=name, default=default, required=required, prefix=prefix,
                call_default=call_default, transform_default=transform_default,
                checker=RegexTypeChecker1())


    setting3 = RegexSetting()


Extending type checker and setting classes
''''''''''''''''''''''''''''''''''''''''''

In the previous example, we combined our own type checker to our own setting
class. But we can extend it furthermore by adding parameters to the type
checker, or by inheriting from previous type checkers.

.. code:: python

    from datetime import datetime
    import re
    import appsettings


    class DateTimeTupleTypeChecker(appsettings.TupleTypeChecker):
        def __init__(
                self, min_length=None, max_length=None, empty=True,
                maximum=None):
            # here we restrict the parent TupleTypeChecker parameters
            # by hard-coding item_type=datetime
            super(DateTimeTupleTypeChecker, self).__init__(
                item_type=datetime, min_length=min_length,
                max_length=max_length, empty=empty)
            # and here we add our custom parameters
            self.maximum = maximum

        # now we are able to extend the check
        def __call__(self, name, value):
            super(DateTimeTupleTypeChecker, self).__call__(name, value)
            if isinstance(self.maximum, datetime):
                for i, item in enumerate(value):
                    if item > self.maximum:
                        raise ValueError(
                            'item %d (%s) in setting %s '
                            'is above maximum %s' % (
                                i, item, name, self.maximum))


    class DateTimeTupleSetting(appsettings.Setting):
        def __init__(
                self, name='', default=lambda: tuple(), prefix='',
                required=False, call_default=True, transform_default=False,
                **checker_kwargs):
            # we simply hook our type checker into our setting class
            super(DateTimeTupleSetting, self).__init__(
                name=name, default=default, required=required, prefix=prefix,
                call_default=call_default, transform_default=transform_default,
                checker=DateTimeTupleTypeChecker(**checker_kwargs))


    setting = DateTimeTupleSetting(
        name='dates_to_remember', default=lambda: (datetime.now(), ),
        min_length=1, maximum=datetime(year=2030, month=1, day=1)


    # and the related setting would be
    DATES_TO_REMEMBER = (
        datetime(year=2017, month=11, day=30),  # the day I wrote this line
    )


Transforming setting values
'''''''''''''''''''''''''''

You may want your setting to be less strict about types, but make sure it
always return the same type of object. This is what the transform method is
here for:

.. code:: python

    import re
    import appsettings


    # our type checker
    class RegexTypeChecker(appsettings.TypeChecker):
        def __init__(self, **kwargs):
            re_type = type(re.compile(r'^$'))
            # allow both str and re_type types
            super(BooleanTypeChecker, self).__init__(base_type=(str, re_type))


    # our setting class
    class RegexSetting(appsettings.Setting):
        def __init__(
                self, name='', default=re.compile(r'^$'), required=False,
                prefix='', call_default=True, transform_default=False,
                **checker_kwargs):
            super(RegexSetting, self).__init__(
                name=name, default=default, required=required, prefix=prefix,
                call_default=call_default, transform_default=transform_default,
                checker=RegexTypeChecker(**checker_kwargs))

        def transform(self, value):
            # ensure it always returns a compiled regex
            if isinstance(value, str):
                value = re.compile(value)
            return value


    setting = RegexSetting()


You can also control whether the default value has to be transformed or not
with the ``transform_default`` parameter. Using the above example, you could
then instantiate your setting like this:

.. code:: python

    setting = RegexSetting(default=r'^my (regular)? expression$',
                           transform_default=True)


You can as well combine ``call_default`` and ``transform_default``:

.. code:: python

    def regex_string_generator():
        return r'^my (regular)? expression$'

    setting = RegexSetting(default=regex_string_generator,
                           call_default=True,
                           transform_default=True)

.. important:: Transformation is always done **after** calling the default value.
