1"""A pytest plugin which helps testing Django applications
2
3This plugin handles creating and destroying the test environment and
4test database and provides some useful text fixtures.
5"""
6
7import contextlib
8import inspect
9from functools import reduce
10import os
11import sys
12import types
13
14import pytest
15
16from .django_compat import is_django_unittest  # noqa
17from .fixtures import django_assert_num_queries  # noqa
18from .fixtures import django_assert_max_num_queries  # noqa
19from .fixtures import django_db_setup  # noqa
20from .fixtures import django_db_use_migrations  # noqa
21from .fixtures import django_db_keepdb  # noqa
22from .fixtures import django_db_createdb  # noqa
23from .fixtures import django_db_modify_db_settings  # noqa
24from .fixtures import django_db_modify_db_settings_parallel_suffix  # noqa
25from .fixtures import django_db_modify_db_settings_tox_suffix  # noqa
26from .fixtures import django_db_modify_db_settings_xdist_suffix  # noqa
27from .fixtures import _live_server_helper  # noqa
28from .fixtures import admin_client  # noqa
29from .fixtures import admin_user  # noqa
30from .fixtures import client  # noqa
31from .fixtures import db  # noqa
32from .fixtures import django_user_model  # noqa
33from .fixtures import django_username_field  # noqa
34from .fixtures import live_server  # noqa
35from .fixtures import django_db_reset_sequences  # noqa
36from .fixtures import rf  # noqa
37from .fixtures import settings  # noqa
38from .fixtures import transactional_db  # noqa
39
40from .lazy_django import django_settings_is_configured, skip_if_no_django
41
42try:
43    import pathlib
44except ImportError:
45    import pathlib2 as pathlib
46
47
48SETTINGS_MODULE_ENV = "DJANGO_SETTINGS_MODULE"
49CONFIGURATION_ENV = "DJANGO_CONFIGURATION"
50INVALID_TEMPLATE_VARS_ENV = "FAIL_INVALID_TEMPLATE_VARS"
51
52PY2 = sys.version_info[0] == 2
53
54# pytest 4.2 handles unittest setup/teardown itself via wrapping fixtures.
55_pytest_version_info = tuple(int(x) for x in pytest.__version__.split(".", 2)[:2])
56_handle_unittest_methods = _pytest_version_info < (4, 2)
57
58_report_header = []
59
60
61# ############### pytest hooks ################
62
63
64def pytest_addoption(parser):
65    group = parser.getgroup("django")
66    group.addoption(
67        "--reuse-db",
68        action="store_true",
69        dest="reuse_db",
70        default=False,
71        help="Re-use the testing database if it already exists, "
72        "and do not remove it when the test finishes.",
73    )
74    group.addoption(
75        "--create-db",
76        action="store_true",
77        dest="create_db",
78        default=False,
79        help="Re-create the database, even if it exists. This "
80        "option can be used to override --reuse-db.",
81    )
82    group.addoption(
83        "--ds",
84        action="store",
85        type=str,
86        dest="ds",
87        default=None,
88        help="Set DJANGO_SETTINGS_MODULE.",
89    )
90    group.addoption(
91        "--dc",
92        action="store",
93        type=str,
94        dest="dc",
95        default=None,
96        help="Set DJANGO_CONFIGURATION.",
97    )
98    group.addoption(
99        "--nomigrations",
100        "--no-migrations",
101        action="store_true",
102        dest="nomigrations",
103        default=False,
104        help="Disable Django migrations on test setup",
105    )
106    group.addoption(
107        "--migrations",
108        action="store_false",
109        dest="nomigrations",
110        default=False,
111        help="Enable Django migrations on test setup",
112    )
113    parser.addini(
114        CONFIGURATION_ENV, "django-configurations class to use by pytest-django."
115    )
116    group.addoption(
117        "--liveserver",
118        default=None,
119        help="Address and port for the live_server fixture.",
120    )
121    parser.addini(
122        SETTINGS_MODULE_ENV, "Django settings module to use by pytest-django."
123    )
124
125    parser.addini(
126        "django_find_project",
127        "Automatically find and add a Django project to the " "Python path.",
128        type="bool",
129        default=True,
130    )
131    group.addoption(
132        "--fail-on-template-vars",
133        action="store_true",
134        dest="itv",
135        default=False,
136        help="Fail for invalid variables in templates.",
137    )
138    parser.addini(
139        INVALID_TEMPLATE_VARS_ENV,
140        "Fail for invalid variables in templates.",
141        type="bool",
142        default=False,
143    )
144
145
146PROJECT_FOUND = (
147    "pytest-django found a Django project in %s "
148    "(it contains manage.py) and added it to the Python path.\n"
149    'If this is wrong, add "django_find_project = false" to '
150    "pytest.ini and explicitly manage your Python path."
151)
152
153PROJECT_NOT_FOUND = (
154    "pytest-django could not find a Django project "
155    "(no manage.py file could be found). You must "
156    "explicitly add your Django project to the Python path "
157    "to have it picked up."
158)
159
160PROJECT_SCAN_DISABLED = (
161    "pytest-django did not search for Django "
162    "projects since it is disabled in the configuration "
163    '("django_find_project = false")'
164)
165
166
167@contextlib.contextmanager
168def _handle_import_error(extra_message):
169    try:
170        yield
171    except ImportError as e:
172        django_msg = (e.args[0] + "\n\n") if e.args else ""
173        msg = django_msg + extra_message
174        raise ImportError(msg)
175
176
177def _add_django_project_to_path(args):
178    def is_django_project(path):
179        try:
180            return path.is_dir() and (path / "manage.py").exists()
181        except OSError:
182            return False
183
184    def arg_to_path(arg):
185        # Test classes or functions can be appended to paths separated by ::
186        arg = arg.split("::", 1)[0]
187        return pathlib.Path(arg)
188
189    def find_django_path(args):
190        args = map(str, args)
191        args = [arg_to_path(x) for x in args if not x.startswith("-")]
192
193        cwd = pathlib.Path.cwd()
194        if not args:
195            args.append(cwd)
196        elif cwd not in args:
197            args.append(cwd)
198
199        for arg in args:
200            if is_django_project(arg):
201                return arg
202            for parent in arg.parents:
203                if is_django_project(parent):
204                    return parent
205        return None
206
207    project_dir = find_django_path(args)
208    if project_dir:
209        sys.path.insert(0, str(project_dir.absolute()))
210        return PROJECT_FOUND % project_dir
211    return PROJECT_NOT_FOUND
212
213
214def _setup_django():
215    if "django" not in sys.modules:
216        return
217
218    import django.conf
219
220    # Avoid force-loading Django when settings are not properly configured.
221    if not django.conf.settings.configured:
222        return
223
224    import django.apps
225
226    if not django.apps.apps.ready:
227        django.setup()
228
229    _blocking_manager.block()
230
231
232def _get_boolean_value(x, name, default=None):
233    if x is None:
234        return default
235    if x in (True, False):
236        return x
237    possible_values = {"true": True, "false": False, "1": True, "0": False}
238    try:
239        return possible_values[x.lower()]
240    except KeyError:
241        raise ValueError(
242            "{} is not a valid value for {}. "
243            "It must be one of {}.".format(x, name, ", ".join(possible_values.keys()))
244        )
245
246
247def pytest_load_initial_conftests(early_config, parser, args):
248    # Register the marks
249    early_config.addinivalue_line(
250        "markers",
251        "django_db(transaction=False): Mark the test as using "
252        "the Django test database.  The *transaction* argument marks will "
253        "allow you to use real transactions in the test like Django's "
254        "TransactionTestCase.",
255    )
256    early_config.addinivalue_line(
257        "markers",
258        "urls(modstr): Use a different URLconf for this test, similar to "
259        "the `urls` attribute of Django's `TestCase` objects.  *modstr* is "
260        "a string specifying the module of a URL config, e.g. "
261        '"my_app.test_urls".',
262    )
263    early_config.addinivalue_line(
264        "markers",
265        "ignore_template_errors(): ignore errors from invalid template "
266        "variables (if --fail-on-template-vars is used).",
267    )
268
269    options = parser.parse_known_args(args)
270
271    if options.version or options.help:
272        return
273
274    django_find_project = _get_boolean_value(
275        early_config.getini("django_find_project"), "django_find_project"
276    )
277
278    if django_find_project:
279        _django_project_scan_outcome = _add_django_project_to_path(args)
280    else:
281        _django_project_scan_outcome = PROJECT_SCAN_DISABLED
282
283    if (
284        options.itv
285        or _get_boolean_value(
286            os.environ.get(INVALID_TEMPLATE_VARS_ENV), INVALID_TEMPLATE_VARS_ENV
287        )
288        or early_config.getini(INVALID_TEMPLATE_VARS_ENV)
289    ):
290        os.environ[INVALID_TEMPLATE_VARS_ENV] = "true"
291
292    def _get_option_with_source(option, envname):
293        if option:
294            return option, "option"
295        if envname in os.environ:
296            return os.environ[envname], "env"
297        cfgval = early_config.getini(envname)
298        if cfgval:
299            return cfgval, "ini"
300        return None, None
301
302    ds, ds_source = _get_option_with_source(options.ds, SETTINGS_MODULE_ENV)
303    dc, dc_source = _get_option_with_source(options.dc, CONFIGURATION_ENV)
304
305    if ds:
306        _report_header.append("settings: %s (from %s)" % (ds, ds_source))
307        os.environ[SETTINGS_MODULE_ENV] = ds
308
309        if dc:
310            _report_header.append("configuration: %s (from %s)" % (dc, dc_source))
311            os.environ[CONFIGURATION_ENV] = dc
312
313            # Install the django-configurations importer
314            import configurations.importer
315
316            configurations.importer.install()
317
318        # Forcefully load Django settings, throws ImportError or
319        # ImproperlyConfigured if settings cannot be loaded.
320        from django.conf import settings as dj_settings
321
322        with _handle_import_error(_django_project_scan_outcome):
323            dj_settings.DATABASES
324
325    _setup_django()
326
327
328def pytest_report_header():
329    if _report_header:
330        return ["django: " + ", ".join(_report_header)]
331
332
333@pytest.mark.trylast
334def pytest_configure():
335    # Allow Django settings to be configured in a user pytest_configure call,
336    # but make sure we call django.setup()
337    _setup_django()
338
339
340def _classmethod_is_defined_at_leaf(cls, method_name):
341    super_method = None
342
343    for base_cls in cls.__mro__[1:]:  # pragma: no branch
344        super_method = base_cls.__dict__.get(method_name)
345        if super_method is not None:
346            break
347
348    assert super_method is not None, (
349        "%s could not be found in base classes" % method_name
350    )
351
352    method = getattr(cls, method_name)
353
354    try:
355        f = method.__func__
356    except AttributeError:
357        pytest.fail("%s.%s should be a classmethod" % (cls, method_name))
358    if PY2 and not (
359        inspect.ismethod(method)
360        and inspect.isclass(method.__self__)
361        and issubclass(cls, method.__self__)
362    ):
363        pytest.fail("%s.%s should be a classmethod" % (cls, method_name))
364    return f is not super_method.__func__
365
366
367_disabled_classmethods = {}
368
369
370def _disable_class_methods(cls):
371    if cls in _disabled_classmethods:
372        return
373
374    _disabled_classmethods[cls] = (
375        # Get the classmethod object (not the resulting bound method),
376        # otherwise inheritance will be broken when restoring.
377        cls.__dict__.get("setUpClass"),
378        _classmethod_is_defined_at_leaf(cls, "setUpClass"),
379        cls.__dict__.get("tearDownClass"),
380        _classmethod_is_defined_at_leaf(cls, "tearDownClass"),
381    )
382
383    cls.setUpClass = types.MethodType(lambda cls: None, cls)
384    cls.tearDownClass = types.MethodType(lambda cls: None, cls)
385
386
387def _restore_class_methods(cls):
388    (
389        setUpClass,
390        restore_setUpClass,
391        tearDownClass,
392        restore_tearDownClass,
393    ) = _disabled_classmethods.pop(cls)
394
395    try:
396        del cls.setUpClass
397    except AttributeError:
398        raise
399
400    try:
401        del cls.tearDownClass
402    except AttributeError:
403        pass
404
405    if restore_setUpClass:
406        cls.setUpClass = setUpClass
407
408    if restore_tearDownClass:
409        cls.tearDownClass = tearDownClass
410
411
412def pytest_runtest_setup(item):
413    if _handle_unittest_methods:
414        if django_settings_is_configured() and is_django_unittest(item):
415            _disable_class_methods(item.cls)
416
417
418@pytest.hookimpl(tryfirst=True)
419def pytest_collection_modifyitems(items):
420    # If Django is not configured we don't need to bother
421    if not django_settings_is_configured():
422        return
423
424    from django.test import TestCase, TransactionTestCase
425
426    def get_order_number(test):
427        if hasattr(test, "cls") and test.cls:
428            # Beware, TestCase is a subclass of TransactionTestCase
429            if issubclass(test.cls, TestCase):
430                return 0
431            if issubclass(test.cls, TransactionTestCase):
432                return 1
433
434        marker_db = test.get_closest_marker('django_db')
435        if marker_db:
436            transaction = validate_django_db(marker_db)[0]
437            if transaction is True:
438                return 1
439        else:
440            transaction = None
441
442        fixtures = getattr(test, 'fixturenames', [])
443        if "transactional_db" in fixtures:
444            return 1
445
446        if transaction is False:
447            return 0
448        if "db" in fixtures:
449            return 0
450
451        return 2
452
453    items[:] = sorted(items, key=get_order_number)
454
455
456@pytest.fixture(autouse=True, scope="session")
457def django_test_environment(request):
458    """
459    Ensure that Django is loaded and has its testing environment setup.
460
461    XXX It is a little dodgy that this is an autouse fixture.  Perhaps
462        an email fixture should be requested in order to be able to
463        use the Django email machinery just like you need to request a
464        db fixture for access to the Django database, etc.  But
465        without duplicating a lot more of Django's test support code
466        we need to follow this model.
467    """
468    if django_settings_is_configured():
469        _setup_django()
470        from django.conf import settings as dj_settings
471        from django.test.utils import setup_test_environment, teardown_test_environment
472
473        dj_settings.DEBUG = False
474        setup_test_environment()
475        request.addfinalizer(teardown_test_environment)
476
477
478@pytest.fixture(scope="session")
479def django_db_blocker():
480    """Wrapper around Django's database access.
481
482    This object can be used to re-enable database access.  This fixture is used
483    internally in pytest-django to build the other fixtures and can be used for
484    special database handling.
485
486    The object is a context manager and provides the methods
487    .unblock()/.block() and .restore() to temporarily enable database access.
488
489    This is an advanced feature that is meant to be used to implement database
490    fixtures.
491    """
492    if not django_settings_is_configured():
493        return None
494
495    return _blocking_manager
496
497
498@pytest.fixture(autouse=True)
499def _django_db_marker(request):
500    """Implement the django_db marker, internal to pytest-django.
501
502    This will dynamically request the ``db``, ``transactional_db`` or
503    ``django_db_reset_sequences`` fixtures as required by the django_db marker.
504    """
505    marker = request.node.get_closest_marker("django_db")
506    if marker:
507        transaction, reset_sequences = validate_django_db(marker)
508        if reset_sequences:
509            request.getfixturevalue("django_db_reset_sequences")
510        elif transaction:
511            request.getfixturevalue("transactional_db")
512        else:
513            request.getfixturevalue("db")
514
515
516@pytest.fixture(autouse=True, scope="class")
517def _django_setup_unittest(request, django_db_blocker):
518    """Setup a django unittest, internal to pytest-django."""
519    if not django_settings_is_configured() or not is_django_unittest(request):
520        yield
521        return
522
523    # Fix/patch pytest.
524    # Before pytest 5.4: https://github.com/pytest-dev/pytest/issues/5991
525    # After pytest 5.4: https://github.com/pytest-dev/pytest-django/issues/824
526    from _pytest.monkeypatch import MonkeyPatch
527
528    def non_debugging_runtest(self):
529        self._testcase(result=self)
530
531    mp_debug = MonkeyPatch()
532    mp_debug.setattr("_pytest.unittest.TestCaseFunction.runtest", non_debugging_runtest)
533
534    request.getfixturevalue("django_db_setup")
535
536    cls = request.node.cls
537
538    with django_db_blocker.unblock():
539        if _handle_unittest_methods:
540            _restore_class_methods(cls)
541            cls.setUpClass()
542            _disable_class_methods(cls)
543
544            yield
545
546            _restore_class_methods(cls)
547            cls.tearDownClass()
548        else:
549            yield
550
551    if mp_debug:
552        mp_debug.undo()
553
554
555@pytest.fixture(scope="function", autouse=True)
556def _dj_autoclear_mailbox():
557    if not django_settings_is_configured():
558        return
559
560    from django.core import mail
561
562    del mail.outbox[:]
563
564
565@pytest.fixture(scope="function")
566def mailoutbox(django_mail_patch_dns, _dj_autoclear_mailbox):
567    if not django_settings_is_configured():
568        return
569
570    from django.core import mail
571
572    return mail.outbox
573
574
575@pytest.fixture(scope="function")
576def django_mail_patch_dns(monkeypatch, django_mail_dnsname):
577    from django.core import mail
578
579    monkeypatch.setattr(mail.message, "DNS_NAME", django_mail_dnsname)
580
581
582@pytest.fixture(scope="function")
583def django_mail_dnsname():
584    return "fake-tests.example.com"
585
586
587@pytest.fixture(autouse=True, scope="function")
588def _django_set_urlconf(request):
589    """Apply the @pytest.mark.urls marker, internal to pytest-django."""
590    marker = request.node.get_closest_marker("urls")
591    if marker:
592        skip_if_no_django()
593        import django.conf
594
595        try:
596            from django.urls import clear_url_caches, set_urlconf
597        except ImportError:
598            # Removed in Django 2.0
599            from django.core.urlresolvers import clear_url_caches, set_urlconf
600
601        urls = validate_urls(marker)
602        original_urlconf = django.conf.settings.ROOT_URLCONF
603        django.conf.settings.ROOT_URLCONF = urls
604        clear_url_caches()
605        set_urlconf(None)
606
607        def restore():
608            django.conf.settings.ROOT_URLCONF = original_urlconf
609            # Copy the pattern from
610            # https://github.com/django/django/blob/master/django/test/signals.py#L152
611            clear_url_caches()
612            set_urlconf(None)
613
614        request.addfinalizer(restore)
615
616
617@pytest.fixture(autouse=True, scope="session")
618def _fail_for_invalid_template_variable():
619    """Fixture that fails for invalid variables in templates.
620
621    This fixture will fail each test that uses django template rendering
622    should a template contain an invalid template variable.
623    The fail message will include the name of the invalid variable and
624    in most cases the template name.
625
626    It does not raise an exception, but fails, as the stack trace doesn't
627    offer any helpful information to debug.
628    This behavior can be switched off using the marker:
629    ``pytest.mark.ignore_template_errors``
630    """
631
632    class InvalidVarException(object):
633        """Custom handler for invalid strings in templates."""
634
635        def __init__(self):
636            self.fail = True
637
638        def __contains__(self, key):
639            """There is a test for '%s' in TEMPLATE_STRING_IF_INVALID."""
640            return key == "%s"
641
642        @staticmethod
643        def _get_origin():
644            stack = inspect.stack()
645
646            # Try to use topmost `self.origin` first (Django 1.9+, and with
647            # TEMPLATE_DEBUG)..
648            for f in stack[2:]:
649                func = f[3]
650                if func == "render":
651                    frame = f[0]
652                    try:
653                        origin = frame.f_locals["self"].origin
654                    except (AttributeError, KeyError):
655                        continue
656                    if origin is not None:
657                        return origin
658
659            from django.template import Template
660
661            # finding the ``render`` needle in the stack
662            frame = reduce(
663                lambda x, y: y[3] == "render" and "base.py" in y[1] and y or x, stack
664            )
665            # assert 0, stack
666            frame = frame[0]
667            # finding only the frame locals in all frame members
668            f_locals = reduce(
669                lambda x, y: y[0] == "f_locals" and y or x, inspect.getmembers(frame)
670            )[1]
671            # ``django.template.base.Template``
672            template = f_locals["self"]
673            if isinstance(template, Template):
674                return template.name
675
676        def __mod__(self, var):
677            """Handle TEMPLATE_STRING_IF_INVALID % var."""
678            origin = self._get_origin()
679            if origin:
680                msg = "Undefined template variable '%s' in '%s'" % (var, origin)
681            else:
682                msg = "Undefined template variable '%s'" % var
683            if self.fail:
684                pytest.fail(msg)
685            else:
686                return msg
687
688    if (
689        os.environ.get(INVALID_TEMPLATE_VARS_ENV, "false") == "true"
690        and django_settings_is_configured()
691    ):
692        from django.conf import settings as dj_settings
693
694        if dj_settings.TEMPLATES:
695            dj_settings.TEMPLATES[0]["OPTIONS"][
696                "string_if_invalid"
697            ] = InvalidVarException()
698        else:
699            dj_settings.TEMPLATE_STRING_IF_INVALID = InvalidVarException()
700
701
702@pytest.fixture(autouse=True)
703def _template_string_if_invalid_marker(request):
704    """Apply the @pytest.mark.ignore_template_errors marker,
705     internal to pytest-django."""
706    marker = request.keywords.get("ignore_template_errors", None)
707    if os.environ.get(INVALID_TEMPLATE_VARS_ENV, "false") == "true":
708        if marker and django_settings_is_configured():
709            from django.conf import settings as dj_settings
710
711            if dj_settings.TEMPLATES:
712                dj_settings.TEMPLATES[0]["OPTIONS"]["string_if_invalid"].fail = False
713            else:
714                dj_settings.TEMPLATE_STRING_IF_INVALID.fail = False
715
716
717@pytest.fixture(autouse=True, scope="function")
718def _django_clear_site_cache():
719    """Clears ``django.contrib.sites.models.SITE_CACHE`` to avoid
720    unexpected behavior with cached site objects.
721    """
722
723    if django_settings_is_configured():
724        from django.conf import settings as dj_settings
725
726        if "django.contrib.sites" in dj_settings.INSTALLED_APPS:
727            from django.contrib.sites.models import Site
728
729            Site.objects.clear_cache()
730
731
732# ############### Helper Functions ################
733
734
735class _DatabaseBlockerContextManager(object):
736    def __init__(self, db_blocker):
737        self._db_blocker = db_blocker
738
739    def __enter__(self):
740        pass
741
742    def __exit__(self, exc_type, exc_value, traceback):
743        self._db_blocker.restore()
744
745
746class _DatabaseBlocker(object):
747    """Manager for django.db.backends.base.base.BaseDatabaseWrapper.
748
749    This is the object returned by django_db_blocker.
750    """
751
752    def __init__(self):
753        self._history = []
754        self._real_ensure_connection = None
755
756    @property
757    def _dj_db_wrapper(self):
758        from django.db.backends.base.base import BaseDatabaseWrapper
759
760        # The first time the _dj_db_wrapper is accessed, we will save a
761        # reference to the real implementation.
762        if self._real_ensure_connection is None:
763            self._real_ensure_connection = BaseDatabaseWrapper.ensure_connection
764
765        return BaseDatabaseWrapper
766
767    def _save_active_wrapper(self):
768        return self._history.append(self._dj_db_wrapper.ensure_connection)
769
770    def _blocking_wrapper(*args, **kwargs):
771        __tracebackhide__ = True
772        __tracebackhide__  # Silence pyflakes
773        raise RuntimeError(
774            "Database access not allowed, "
775            'use the "django_db" mark, or the '
776            '"db" or "transactional_db" fixtures to enable it.'
777        )
778
779    def unblock(self):
780        """Enable access to the Django database."""
781        self._save_active_wrapper()
782        self._dj_db_wrapper.ensure_connection = self._real_ensure_connection
783        return _DatabaseBlockerContextManager(self)
784
785    def block(self):
786        """Disable access to the Django database."""
787        self._save_active_wrapper()
788        self._dj_db_wrapper.ensure_connection = self._blocking_wrapper
789        return _DatabaseBlockerContextManager(self)
790
791    def restore(self):
792        self._dj_db_wrapper.ensure_connection = self._history.pop()
793
794
795_blocking_manager = _DatabaseBlocker()
796
797
798def validate_django_db(marker):
799    """Validate the django_db marker.
800
801    It checks the signature and creates the ``transaction`` and
802    ``reset_sequences`` attributes on the marker which will have the
803    correct values.
804
805    A sequence reset is only allowed when combined with a transaction.
806    """
807
808    def apifun(transaction=False, reset_sequences=False):
809        return transaction, reset_sequences
810
811    return apifun(*marker.args, **marker.kwargs)
812
813
814def validate_urls(marker):
815    """Validate the urls marker.
816
817    It checks the signature and creates the `urls` attribute on the
818    marker which will have the correct value.
819    """
820
821    def apifun(urls):
822        return urls
823
824    return apifun(*marker.args, **marker.kwargs)
825