File: configuration.py

package info (click to toggle)
python-pytest-djangoapp 1.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 396 kB
  • sloc: python: 1,116; makefile: 114; sh: 6
file content (304 lines) | stat: -rw-r--r-- 9,259 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
from threading import local
from typing import Callable

_THREAD_LOCAL = local()
setattr(_THREAD_LOCAL, 'configuration', {})


class FakeMigrationModules:
    """Allows skipping migration applying process."""

    def __init__(self, module_name: str):
        self.module_name = module_name

    def __getitem__(self, item: str) -> str:
        return self.module_name

    def __contains__(self, item: str):
        return True


class Configuration:

    _prefix = 'DJANGOAPP_OPTIONS'
    _KEY_ADMIN = 'admin'
    KEY_APP = 'app_name'
    _KEY_EXTEND = 'extend'
    _KEY_HOOK = 'hook'
    _KEY_MIGRATE = 'migrate'

    DIR_TESTAPP = 'testapp'
    """Name of test application directory.
    
    Test application directory should be placed inside `tests` directory 
    and needs to be a Python package (contain __init__.py).
     
    Test application is useful to place there modules like `urls.py`,
    `models.py` (e.g. with custom models), etc.
    
    """

    @classmethod
    def get(cls) -> dict:
        return _THREAD_LOCAL.configuration

    @classmethod
    def set(
        cls,
        settings_dict: dict = None,
        *,
        app_name: str = None,
        admin_contrib: bool = False,
        settings_hook: Callable = None,
        migrate: bool = True,
        **kwargs
    ):
        """
        :param settings_dict:

        :param app_name:

        :param admin_contrib: Setup Django to test Admin contrib related parts.

        :param settings_hook: Allows setting a function to get resulting settings.

            Function must accept settings dictionary, and return resulting settings dictionary.

            .. code-block:: python

                def hook_func(settings):
                    return settings

        :param migrate: Allows applying or skipping migration applying process.
            Skipping could be useful for testing applications with many migrations.

        :param kwargs: Additional arguments.

            Use `extend_` prefix to extend default configuration.
            E.g.: extend_INSTALLED_APPS=['a']

        """
        settings_dict = settings_dict or {}

        extend = {}

        for key, val in kwargs.items():
            _, _, extend_key = key.partition('extend_')

            if extend_key and extend_key == extend_key.upper():
                extend[extend_key] = val

        base_settings = {
            cls._prefix: {
                cls.KEY_APP: app_name,
                cls._KEY_EXTEND: extend,
                cls._KEY_ADMIN: admin_contrib,
                cls._KEY_HOOK: settings_hook,
                cls._KEY_MIGRATE: migrate,
            }
        }

        base_settings.update(settings_dict)

        _THREAD_LOCAL.configuration = base_settings

    @classmethod
    def get_defaults(cls) -> dict:
        from django.conf import global_settings

        if hasattr(global_settings, 'MIDDLEWARE_CLASSES'):
            middleware = global_settings.MIDDLEWARE_CLASSES

        else:
            middleware = global_settings.MIDDLEWARE

        installed_apps = list(global_settings.INSTALLED_APPS[:])
        installed_apps.extend([
            'django.contrib.auth',
            'django.contrib.contenttypes',
        ])
        installed_apps = list(set(installed_apps))

        settings_dict = dict(

            SECRET_KEY='djangoapp',

            ALLOWED_HOSTS=(
                global_settings.ALLOWED_HOSTS +
                # Satisfy Django test client needed in Django < 2.0
                ['testserver']
            ),

            INSTALLED_APPS=installed_apps,
            STATIC_URL='/static/',

            DATABASES={
                'default': {
                    'ENGINE': 'django.db.backends.sqlite3',
                    'NAME': ':memory:',
                },
            },

            MIDDLEWARE=middleware,

            EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend',

            TEMPLATES=[
                {
                    'BACKEND': 'django.template.backends.django.DjangoTemplates',
                    'APP_DIRS': True,
                    'OPTIONS': {'context_processors': ['django.contrib.messages.context_processors.messages']}
                },
            ],

        )

        return settings_dict.copy()

    @classmethod
    def get_combined(cls, pytest_config) -> dict:
        from django import VERSION

        settings = cls.get()

        defaults = cls.get_defaults()
        defaults.update(settings)

        djapp_options = defaults[cls._prefix]

        app_name = djapp_options[cls.KEY_APP]
        extensions = djapp_options[cls._KEY_EXTEND]
        admin = djapp_options[cls._KEY_ADMIN]
        hook = djapp_options.pop(cls._KEY_HOOK, None) or (lambda settings_dict: settings_dict)

        # djangoapp is considered testing a whole project (a set of apps)
        # if hook function is a `partial` for function with a certain name.
        project_mode = getattr(getattr(hook, 'func', None), '__name__', '') == 'update_settings_from_module'

        if not djapp_options[cls._KEY_MIGRATE]:
            module_name = None

            if VERSION <= (1, 10):
                module_name = 'dummy_migrations'
                defaults['MIGRATIONS_MODULE_NAME'] = module_name

            defaults['MIGRATION_MODULES'] = FakeMigrationModules(module_name)

        if admin:
            middleware = extensions.setdefault('MIDDLEWARE', [])
            middleware.extend([
                'django.contrib.sessions.middleware.SessionMiddleware',
                'django.contrib.auth.middleware.AuthenticationMiddleware',
            ])
            apps = extensions.setdefault('INSTALLED_APPS', [])
            apps.extend([
                'django.contrib.admin',
                'django.contrib.sessions',
            ])

        for key, value in extensions.items():
            default_value = defaults.get(key, [])

            if isinstance(default_value, (list, tuple)):
                extended = list(default_value)

                for item in value:
                    if item not in extended:
                        extended.append(item)

                defaults[key] = extended

                if key == 'MIDDLEWARE':
                    # Special case for renamed.
                    defaults['MIDDLEWARE_CLASSES'] = extended

            elif isinstance(default_value, dict):
                defaults[key].update(value)

            else:  # pragma: nocover
                raise ValueError(f'Unable to extend `{key}` option.')

        installed_apps = defaults['INSTALLED_APPS']

        if app_name:

            if app_name not in installed_apps:
                installed_apps.append(app_name)

        else:
            dir_current = pytest_config.invocation_dir
            dir_tests = None

            app_name = dir_current.basename

            if app_name == 'tests':
                # Support certain module or function invocation tests dir as base (e.g. PyCharm behaviour).
                app_name = dir_current.parts()[-2].basename
                dir_tests = dir_current

            try:
                dir_tests = dir_current.listdir('tests')[0]

            except IndexError:
                pass

            if not dir_tests:
                # No `tests` subdir found. Let's try to deduce.

                app_name = None

                from setuptools import find_packages
                import py

                candidate_latest = ''
                candidates = []

                for package in find_packages(f'{dir_current}'):
                    # Consider only top level packages.
                    if not candidate_latest or not package.startswith(candidate_latest):
                        candidates.append(package)
                        candidate_latest = package

                for candidate in candidates:
                    dirs = py.path.local(candidate).listdir('tests')

                    if dirs:
                        app_name = candidate
                        dir_tests = dirs[0]
                        break

            if not app_name and not project_mode:
                raise Exception(
                    'Unable to deduce application name. '
                    'Check application package and `tests` directory exists. '
                    f'Current dir: {dir_current}')

            if app_name:
                installed_apps.append(app_name)

            if dir_tests:
                # Try to find and add an additional test app.
                dir_testapp_name = cls.DIR_TESTAPP
                dir_testapp = dir_tests.listdir(dir_testapp_name)

                if dir_testapp:
                    dir_testapp = dir_testapp[0]

                    testapp_name = f'{app_name}.tests.{dir_testapp_name}'

                    installed_apps.append(testapp_name)

                    if dir_testapp.listdir('urls.py'):
                        # Set customized `urls.py`.
                        defaults['ROOT_URLCONF'] = f'{testapp_name}.urls'

        djapp_options[cls.KEY_APP] = app_name

        defaults = hook(defaults)

        return defaults

    @classmethod
    def get_dict(cls) -> dict:
        """Returns current configuration as a dict."""
        return cls.get()[cls._prefix]