Django

Installation

# dnf install -y python-django-bash-completion
$ workon shmakovpn
$ pip install django

Warning

If you want to use SQLite with Django under Centos 7, so it doesn’t work out of the box. Please, read DjangoCentos7SQLite to understand how to solve this.

Note

Centos 8 doesn’t have problems with SQLite.

Starting a new Django project

Create a new directory for Django project

$ mkdir ~/projects/shmakovpn_tools/shmakovpn/django_examples

Start a new Django project

$ django-admin startproject django_examples ~/projects/shmakovpn_tools/shmakovpn/django_examples

Initialize database

$ python ~/projects/shmakovpn_tools/shmakovpn/django_examples/manage.py migrate
Operations to perform:
...
...
  Appying sessions.0001_initial... OK

A simple application with logging

Imagine two persons. A developer and an Administrator. First of them writes code in a sandbox, or other words, in an isolated testing environment. He knows about code anything. But he doesn’t work in real life, there are many factors able to make the application works something wrong: hardware, network, software updates, stupid users, natural disasters and etc. The Administrator lives that life. He is a person, who knows nothing about code. Maybe, he even hates code. And he isn’t one of them who fixes bugs in it.

The application has to include a log system. If something wrong has happened, an Administrator increases the logging level then sends logs to developers.

Let’s take a look at a simple Django application that includes a multi-level log system.

Start a new Django application

$ mkdir ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example
$ django-admin startapp logging_example ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example

Edit INSTALLED_APPS section in the Django project settings.py file

$ vim ~/projets/shmakovpn_tools/shmakovpn/django_examples/django_examples/settings.py

Add logging_example to INSTALLED_APPS

INSTALLED_APPS = [
    ...
    ...
    'logging_example',
]

Edit the __init__.py file of the created application

$ vim ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example/__init__.py
"""
shmakovpn_tools/shmakovpn/django_examples/logging_example/__init__.py

Author: shmakovpn <shmakovpn@yandex.ru>
Date: 2020-09-18
"""
import logging
from logging import Logger

logger: Logger = logging.getLogger('logging_example')

Note

A tests.py file is created inside a Django application by default. But as the application grows, this file becomes too large. So, we will not use it, we are going to split tests to several packages and files.

python tests structure

Django (python) tests structure

Create tests python package inside the application folder

$ mkdir ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example/tests
$ touch ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example/tests/__init__.py

Create logger_itself python package inside the application folder

$ mkdir ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example/tests/logger_itself
$ touch ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example/tests/logger_itself/__init__.py

Delete default tests.py

$ rm ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example/tests.py

Create a test

$ vim ~/projects/shmakovpn_tools/shmakovpn/django_examples/logging_example/tests/logger_itself/test_logger_itself.py
from unittest import TestCase


class TestLoggerItself(TestCase):
    def test_logger_itself(self) -> None:
        print('hello from test')

Run test

$ python ~/projects/shmakovpn_tools/shmakovpn/django_examples/manage.py test logging_example
System check identified no issues (0 silenced).
hello from test
.
----------------
Ran 1 test in 0.000s

OK

Install unittest_dataprovider

$ pip install unittest-dataprovider

We will test logger in two cases: DEBUG_MODE=True and DEBUG_MODE=False. Each case should be performed for all standard levels of logging: debug, info, warning, error, and critical. To realize this, we can write 5 test functions, but this approach violates the DRY (don’t repeat yourself) principle. The dataprovider decorator can make code clear and simple.

Also, the dataprovider needs data. The easiest way to generate the data is itertools.product.

We will use override_settings context manager to change the project configuration inside the test.

It is not enough to override_settings. Because logging is configured before override_settings take effect. Thus, it is need to call configure_logging to change logging settings in fact.

Warning

Don’t forget to return the original logging settings back.

To catch writings in stderr we have to patch sys.stderr using MagicMock. But to stay writing to stderr itself, the patched object has to be wrapped as sys.stderr.

"""
shmakovpn_tools
 /shmakovpn
  /django_examples
   /logging_example
    /tests
     /logger_iftelf
      /test_logger_itself.py

Author: shmakovpn <shmakovpn@yandex.ru>
Date: 2020-09-18
"""
import sys
import logging
from itertools import product
from django.test import TestCase, override_settings
from unittest.mock import MagicMock, patch, call
from unittest_dataprovider import data_provider
from django.conf import settings

# I put this string in order to make **override_settings(LOGGING=...)*** works.
# Because it is not enough to use only **override_settings(LOGGING=...)**,
# if you want to change the logging settings in your test cases.
from django.utils.log import configure_logging

# type hints
from typing import List, Tuple, Dict

#: define type for levels of logging
LoggingLevel = int
#: define type for statuses of debug
DebugStatus = bool
#: List of all possible debug statuses
DEBUG_STATUSES: List[DebugStatus] = [True, False, ]
#: List of all standard levels of logging
LOGGING_LEVELS: List[LoggingLevel] = [
    logging.DEBUG,
    logging.INFO,
    logging.WARNING,
    logging.ERROR,
    logging.CRITICAL,
]


class TestLoggerItself(TestCase):
    @staticmethod
    def data() -> List[Tuple[DebugStatus, LoggingLevel]]:
        """ Dataprovider for testing logger itself """
        return list(
            product(
                DEBUG_STATUSES,
                LOGGING_LEVELS,
            )
        )

    @staticmethod
    def get_logging_configuration(
            debug_status: DebugStatus,
            log_level: LoggingLevel,
    ):
        """
        Creates a logging configuration for a Django project
        """
        return {
            'version': 1,
            'disable_existing_loggers': False,
            'formatters': {
                'formatter1': {
                    'format': ' -> {levelname} {asctime} {module} {process:d} {thread:d} {message}',
                    'style': '{',
                },
                'formatter2': {
                    'format': f'debug={debug_status} log_level={logging.getLevelName(log_level)} ' + '{message}',
                    'style': '{',
                }
            },
            'handlers': {
                'stream': {
                    'level': logging.getLevelName(log_level),
                    'class': 'logging.StreamHandler',
                    'formatter': 'formatter1',
                },
                'stream_header': {
                    'level': 'DEBUG',
                    'class': 'logging.StreamHandler',
                    'formatter': 'formatter2',
                }
            },
            'loggers': {
                __name__: {
                    'handlers': ['stream'],
                    'level': logging.getLevelName(log_level),
                    'propogate': True,
                },
                f'header_{__name__}': {
                    'handlers': ['stream_header'],
                    'level': 'DEBUG',
                    'propogare': True,
                }
            },
        }

    @data_provider(data)
    @patch(
        target='sys.stderr',
        wraps=sys.stderr,  # wraps all of sys.stderr
    )
    def test_logger_itself(
            self,
            debug_status: DebugStatus,
            log_level: LoggingLevel,
            mocked_stderr: MagicMock,
    ) -> None:
        with override_settings(
                DEBUG=debug_status,
                LOGGING=TestLoggerItself.get_logging_configuration(
                    debug_status,
                    log_level,
                ),
        ):
            configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)  # really change logging settings
            logger_header: logging.Logger = logging.getLogger(f'header_{__name__}')
            logger_header.debug(f'')  # write *header* to stderr
            mocked_stderr.reset_mock()  # the test doesn't start yet, resetting the mock object
            logger: logging.Logger = logging.getLogger(__name__)
            level: LoggingLevel  # type hints
            for level in LOGGING_LEVELS:
                logger.log(level, f'hello "{logging.getLevelName(log_level)}"')
                if level >= log_level:
                    self.assertTrue(mocked_stderr.method_calls)
                else:
                    self.assertFalse(mocked_stderr.method_calls)
                mocked_stderr.reset_mock()
        configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)  # return to the native settings back
Django logging test result