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