behave-django
behave-django copied to clipboard
Recursion error on `django.conf.UserSettingsHolder.__getattr__` (or other) since Django 4.1 after many feature tests on PostgreSQL DB
tl;dr: Each scenario adds a new recursion level in multiple places (usually the settings fetch), and ends up throwing RecursionError: maximum recursion depth exceeded. The error can be replicated with a vanilla Django project from Django 4.1 onward. I am unable to find a culprit and require help from someone to investigate further. The issue doesn't happen on vanilla LiveServerTestCase unit tests. The recursion error is not consistently in the same place.
Background
After bumping from Django 5.0.x to 5.1 in a project, our feature tests started to fail after ~50min with the error pasted in the collapsed menu below. Turns out once too many tests are ran, a recursion error happens.
Click to expand!
Scenario Outline: server-side validation with invalid data: Injection Post above maximum -- @1.14 # FeatureTests/features/update_gfr.feature:134
Given I am logged in as clinician1 # FeatureTests/steps/login.py:17
2025-06-17 09:54:00,092 - ERROR - django.request - Internal Server Error: /accounts/login/
Traceback (most recent call last):
return self._cursor()
File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 296, in _cursor
self.ensure_connection()
File "/usr/local/lib/python3.10/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
[Previous line repeated 319 more times]
File "/usr/local/lib/python3.10/site-packages/django/utils/asyncio.py", line 26, in inner
return func(*args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 279, in ensure_connection
self.connect()
File "/usr/local/lib/python3.10/site-packages/django/utils/asyncio.py", line 26, in inner
return func(*args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/base/base.py", line 255, in connect
conn_params = self.get_connection_params()
File "/usr/local/lib/python3.10/site-packages/django/db/backends/postgresql/base.py", line 295, in get_connection_params
settings.USE_TZ, self.timezone
File "/usr/local/lib/python3.10/site-packages/django/conf/__init__.py", line 83, in __getattr__
val = getattr(_wrapped, name)
File "/usr/local/lib/python3.10/site-packages/django/conf/__init__.py", line 235, in __getattr__
return getattr(self.default_settings, name)
File "/usr/local/lib/python3.10/site-packages/django/conf/__init__.py", line 235, in __getattr__
return getattr(self.default_settings, name)
File "/usr/local/lib/python3.10/site-packages/django/conf/__init__.py", line 235, in __getattr__
return getattr(self.default_settings, name)
[Previous line repeated 292 more times]
RecursionError: maximum recursion depth exceeded
Reproduction
This super plain Django project reproduces the error. It has:
- Dependencies managed with Poetry
- The latest Django version
- Starts a PostgreSQL DB in docker-compose
- No models
- Just 1 "Scenario Outline" calling 1 step that does nothing 1000+ times
- The recursion limit set to
sys.setrecursionlimit(300)in the settings.py to fail a bit faster.
The project: django_issue.zip
To use: unzip and:
- run
make allto run the 1000+ repeated feature tests - run
make run-unit-teststo compare with 1200 unit tests not failing - Change
pyproject.tomlline for django todjango = "4.1"to test with Django 4.1 (it will use behave-django 1.4.0) or<4.1to check with the version that doesn't fail.- Then
poetry updatethenmake allto replicate the issue
- Then
Here is the end of the output of make all. Note that the stack is different than the above error. The error is not really consistently in the same place, meaning there are recursion errors in several places.
Click to expand!
Scenario Outline: A plain scenario -- @1.131 # features/feature_one.feature:138
Given I run a step # features/steps/steps.py:3 0.000s
Scenario Outline: A plain scenario -- @1.132 # features/feature_one.feature:139
Given I run a step # features/steps/steps.py:3 0.000s
Scenario Outline: A plain scenario -- @1.133 # features/feature_one.feature:140
Given I run a step # features/steps/steps.py:3 0.000s
Exception RecursionError: maximum recursion depth exceeded
Traceback (most recent call last):
File "/home/paul/git/REDACTED/django_issue/manage.py", line 22, in <module>
main()
~~~~^^
File "/home/paul/git/REDACTED/django_issue/manage.py", line 18, in main
execute_from_command_line(sys.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
utility.execute()
~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 436, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 416, in run_from_argv
self.execute(*args, **cmd_options)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 460, in execute
output = self.handle(*args, **options)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave_django/management/commands/behave.py", line 197, in handle
exit_status = behave_main(args=behave_args)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/__main__.py", line 290, in main
return run_behave(config)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/__main__.py", line 112, in run_behave
failed = runner.run()
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/runner.py", line 936, in run
return self.run_with_paths()
~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/runner.py", line 956, in run_with_paths
return self.run_model()
~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/runner.py", line 757, in run_model
failed = feature.run(self)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/model.py", line 418, in run
failed = run_item.run(runner)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/model.py", line 1668, in run
failed = scenario.run(runner)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/model.py", line 1231, in run
runner.run_hook("after_scenario", runner.context, self)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave_django/environment.py", line 147, in run_hook
django_test_runner.teardown_test(context)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave_django/environment.py", line 124, in teardown_test
context.test._post_teardown(run=True)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave_django/testcase.py", line 20, in _post_teardown
super()._post_teardown()
~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 1227, in _post_teardown
self._fixture_teardown()
~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 1262, in _fixture_teardown
call_command(
~~~~~~~~~~~~^
"flush",
^^^^^^^^
...<5 lines>...
inhibit_post_migrate=inhibit_post_migrate,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 194, in call_command
return command.execute(*args, **defaults)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 460, in execute
output = self.handle(*args, **options)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/commands/flush.py", line 52, in handle
sql_list = sql_flush(
self.style,
...<2 lines>...
allow_cascade=allow_cascade,
)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/sql.py", line 11, in sql_flush
tables = connection.introspection.django_table_names(
only_existing=True, include_views=False
)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/introspection.py", line 110, in django_table_names
existing_tables = set(self.table_names(include_views=include_views))
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/introspection.py", line 56, in table_names
with self.connection.cursor() as cursor:
~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/utils/asyncio.py", line 26, in inner
return func(*args, **kwargs)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/base.py", line 320, in cursor
return self._cursor()
~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/base.py", line 296, in _cursor
self.ensure_connection()
~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
[Previous line repeated 130 more times]
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/utils/asyncio.py", line 26, in inner
return func(*args, **kwargs)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/base.py", line 279, in ensure_connection
self.connect()
~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/utils/asyncio.py", line 26, in inner
return func(*args, **kwargs)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/base.py", line 257, in connect
self.set_autocommit(self.settings_dict["AUTOCOMMIT"])
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/base.py", line 473, in set_autocommit
self.ensure_connection()
~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 311, in patched_ensure_connection
real_ensure_connection(self, *args, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
[Previous line repeated 130 more times]
RecursionError: maximum recursion depth exceeded
make: *** [Makefile:31: run-feature-tests] Error 1
And here with Django 4.1:
Click to expand!
Scenario Outline: A plain scenario -- @1.267 # features/feature_one.feature:274
Given I run a step # features/steps/steps.py:3 0.000s
Scenario Outline: A plain scenario -- @1.268 # features/feature_one.feature:275
Given I run a step # features/steps/steps.py:3 0.000s
Scenario Outline: A plain scenario -- @1.269 # features/feature_one.feature:276
Given I run a step # features/steps/steps.py:3 0.000s
Exception RecursionError: maximum recursion depth exceeded
Traceback (most recent call last):
File "/home/paul/git/REDACTED/django_issue/manage.py", line 22, in <module>
main()
~~~~^^
File "/home/paul/git/REDACTED/django_issue/manage.py", line 18, in main
execute_from_command_line(sys.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 446, in execute_from_command_line
utility.execute()
~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 440, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 402, in run_from_argv
self.execute(*args, **cmd_options)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 448, in execute
output = self.handle(*args, **options)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave_django/management/commands/behave.py", line 150, in handle
exit_status = behave_main(args=behave_args)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/__main__.py", line 183, in main
return run_behave(config)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/__main__.py", line 127, in run_behave
failed = runner.run()
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/runner.py", line 804, in run
return self.run_with_paths()
~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/runner.py", line 824, in run_with_paths
return self.run_model()
~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/runner.py", line 626, in run_model
failed = feature.run(self)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/model.py", line 321, in run
failed = scenario.run(runner)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/model.py", line 1114, in run
failed = scenario.run(runner)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave/model.py", line 758, in run
runner.run_hook("after_scenario", runner.context, self)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave_django/environment.py", line 126, in run_hook
django_test_runner.teardown_test(context)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave_django/environment.py", line 103, in teardown_test
context.test._post_teardown(run=True)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/behave_django/testcase.py", line 20, in _post_teardown
super(BehaviorDrivenTestMixin, self)._post_teardown()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 1270, in _post_teardown
self._fixture_teardown()
~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/test/testcases.py", line 1304, in _fixture_teardown
call_command(
~~~~~~~~~~~~^
"flush",
^^^^^^^^
...<5 lines>...
inhibit_post_migrate=inhibit_post_migrate,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 198, in call_command
return command.execute(*args, **defaults)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 448, in execute
output = self.handle(*args, **options)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/commands/flush.py", line 51, in handle
sql_list = sql_flush(
self.style,
...<2 lines>...
allow_cascade=allow_cascade,
)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/core/management/sql.py", line 11, in sql_flush
tables = connection.introspection.django_table_names(
only_existing=True, include_views=False
)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/introspection.py", line 110, in django_table_names
existing_tables = set(self.table_names(include_views=include_views))
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/introspection.py", line 56, in table_names
with self.connection.cursor() as cursor:
~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/utils/asyncio.py", line 26, in inner
return func(*args, **kwargs)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/base.py", line 323, in cursor
return self._cursor()
~~~~~~~~~~~~^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/base/base.py", line 301, in _cursor
return self._prepare_cursor(self.create_cursor(name))
~~~~~~~~~~~~~~~~~~^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/utils/asyncio.py", line 26, in inner
return func(*args, **kwargs)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/db/backends/postgresql/base.py", line 270, in create_cursor
cursor.tzinfo_factory = self.tzinfo_factory if settings.USE_TZ else None
^^^^^^^^^^^^^^^
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/conf/__init__.py", line 94, in __getattr__
val = getattr(_wrapped, name)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/conf/__init__.py", line 270, in __getattr__
return getattr(self.default_settings, name)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/conf/__init__.py", line 270, in __getattr__
return getattr(self.default_settings, name)
File "/home/paul/git/REDACTED/django_issue/.venv/lib/python3.13/site-packages/django/conf/__init__.py", line 270, in __getattr__
return getattr(self.default_settings, name)
[Previous line repeated 265 more times]
RecursionError: maximum recursion depth exceeded
make: *** [Makefile:31: run-feature-tests] Error 1
Debugging
I recommend adding a breakpoint in your venv's django.conf.UserSettingsHolder.__getattr__, make it conditional like name == "DEBUG" and observe the stack having 1 more __getattr__ call per test. These are the kind of stacks I saw:
Here in Django 5
__getattr__, __init__.py:236
__getattr__, __init__.py:241
__getattr__, __init__.py:241
__getattr__, __init__.py:83
get_connection_params, base.py:295
connect, base.py:255
inner, asyncio.py:26
ensure_connection, base.py:279
inner, asyncio.py:26
patched_ensure_connection, testcases.py:311
patched_ensure_connection, testcases.py:311
patched_ensure_connection, testcases.py:311
patched_ensure_connection, testcases.py:311
_cursor, base.py:296
cursor, base.py:320
inner, asyncio.py:26
execute_sql, compiler.py:1621
__iter__, query.py:91
_fetch_all, query.py:1949
__len__, query.py:366
get, query.py:629
manager_method, manager.py:87
get_by_natural_key, base_user.py:37
authenticate, backends.py:65
authenticate, __init__.py:114
sensitive_variables_wrapper, debug.py:75
clean, forms.py:366
sensitive_variables_wrapper, debug.py:75
_clean_form, forms.py:354
full_clean, forms.py:338
errors, forms.py:201
is_valid, forms.py:206
post, edit.py:150
dispatch, base.py:144
dispatch, views.py:89
_view_wrapper, cache.py:80
_wrapper, decorators.py:48
_view_wrapper, decorators.py:190
_wrapper, decorators.py:48
sensitive_post_parameters_wrapper, debug.py:143
_wrapper, decorators.py:48
_wrapper, decorators.py:48
view, base.py:105
_get_response, base.py:197
inner, exception.py:55
__call__, deprecation.py:120
inner, exception.py:55
__call__, deprecation.py:120
inner, exception.py:55
__call__, deprecation.py:120
inner, exception.py:55
__call__, deprecation.py:120
inner, exception.py:55
__call__, deprecation.py:120
inner, exception.py:55
__call__, deprecation.py:120
inner, exception.py:55
__call__, middleware.py:123
inner, exception.py:55
__call__, deprecation.py:120
inner, exception.py:55
__call__, deprecation.py:120
inner, exception.py:55
__call__, middleware.py:56
inner, exception.py:55
get_response, base.py:140
__call__, wsgi.py:124
__call__, testcases.py:1695
__call__, handlers.py:80
run, handlers.py:137
handle_one_request, basehttp.py:253
handle, basehttp.py:230
__init__, socketserver.py:766
finish_request, socketserver.py:362
process_request_thread, socketserver.py:697
process_request_thread, basehttp.py:103
run, threading.py:992
_bootstrap_inner, threading.py:1041
_bootstrap, threading.py:1012
Here in Django 4.1
__getattr__, __init__.py:270
__getattr__, __init__.py:270
__getattr__, __init__.py:270
__getattr__, __init__.py:270
__getattr__, __init__.py:94
__init__, middleware.py:15
load_middleware, base.py:61
__init__, wsgi.py:125
run, testcases.py:1747
_bootstrap_inner, threading.py:1016
_bootstrap, threading.py:973
Another one in 4.1
__getattr__, __init__.py:270
__getattr__, __init__.py:270
__getattr__, __init__.py:270
__getattr__, __init__.py:94
swapped, options.py:413
get_models, config.py:258
<listcomp>, utils.py:278
get_migratable_models, utils.py:278
<genexpr>, introspection.py:87
django_table_names, introspection.py:99
sql_flush, sql.py:11
handle, flush.py:51
execute, base.py:448
call_command, __init__.py:198
_fixture_teardown, testcases.py:1304
_post_teardown, testcases.py:1270
_post_teardown, testcase.py:20
teardown_test, environment.py:103
run_hook, environment.py:126
run, model.py:758
run, model.py:1114
run, model.py:321
run_model, runner.py:626
run_with_paths, runner.py:824
run, runner.py:804
run_behave, __main__.py:127
main, __main__.py:183
handle, behave.py:150
execute, base.py:448
run_from_argv, base.py:402
execute, __init__.py:440
run_as_django_behave, _jb_django_behave.py:38
<module>, behave_runner.py:331
And a normal one in 4.0
# Note: there is only ever 1 __getattr__ called after that
swapped, options.py:417
get_models, config.py:293
<listcomp>, utils.py:301
get_migratable_models, utils.py:301
<genexpr>, introspection.py:87
django_table_names, introspection.py:99
sql_flush, sql.py:11
handle, flush.py:51
execute, base.py:460
call_command, __init__.py:198
_fixture_teardown, testcases.py:1230
_post_teardown, testcases.py:1196
_post_teardown, testcase.py:20
teardown_test, environment.py:103
run_hook, environment.py:126
run, model.py:758
run, model.py:1114
run, model.py:321
run_model, runner.py:626
run_with_paths, runner.py:824
run, runner.py:804
run_behave, __main__.py:127
main, __main__.py:183
handle, behave.py:150
execute, base.py:460
run_from_argv, base.py:414
execute, __init__.py:440
run_as_django_behave, _jb_django_behave.py:38
<module>, behave_runner.py:331
I can't find anything relevant on their release notes. Nor in Django's code where they seem to do some wrapping, git blame shows code that's not been touched in more than a decade.
In a debugger, I could get close to something that made sense: in a breakpoint here with some extra prints added in __getattr__, I could run the following in the debug console and see 1 more recursion layer every time I would run the following:
override = UserSettingsHolder(settings._wrapped)
settings._wrapped = override
override.USE_TZ
That seems useful but... Basically I don't know where to look exactly.
Also: no issue when using SQLite
Hi Paul, thanks for your effort in reporting this problem! You've invested heavily, that's apparent.
Please note that you're on the issue tracker of the behave-django project. Make sure you have strong pointers that indicate that this project is involved in the problem—according to your analysis this is not the case; behave or behave-django being only the execution starting point does not account for a strong indicator, IMHO. Please correct me if I'm missing relevant details.
FWICS, you've figured that the problem is caused by a Django version change if and only if a Postgres database is used. You may also want to verify that this problem wasn't introduced by the Python 3.13 version you use (see https://github.com/behave/behave-django/issues/163#issuecomment-2828802538 for related links).
In the end, it might make sense to post your report on the Django issue tracker.
Apologies for not having made it clearer.
I can only replicate the issue if it is using a feature test. A loose suspicion is around how behave-django alters the test setup / teardown aspect of the LiveServerTestCase. I was worried that if I went to the Django issue tracker, they'd tell me it's a behave-django issue.
My initial post mentioned creating unit-testss with LiveServerTestCase and not failing, and that was my justification. But I just tried with BehaviorDrivenTestCase and there is no issue. So this is still an indicator something happens only around a feature test
I should have also mentioned that I did try with Python 3.10 too with similar results.
OK I believe I got the issue narrowed down further. I think it is a behave-django issue: doClassCleanups is never called at the end of a scenario, leading to random issues piling up.
I may be wrong but don't think it's behave responsibility to call it as it doesn't know what test class we are using.
Python unit tests are using their own TestSuite class to call this cleanup stage: doClassCleanups is defined here, called in _tearDownPreviousClass, which itself is called in the Python default TestSuite.
Am I right thinking behave-django should deal with calling doClassCleanups somewhere then?
A loose suspicion is around how behave-django alters the test setup / teardown aspect of the
LiveServerTestCase.
But I just tried with BehaviorDrivenTestCase and there is no issue.
If there is no issue with BehaviorDrivenTestCase, i.e. the default behave-django setup, there is one place less to look at. Now, look at the remaining places.
I recommend that you strip down your entire setup to construct a minimal, reproducible example. There's a lot going on in your setup, which is in essence optional (Makefile, Docker Compose, database in a container, etc.) and could be removed to reduce the noise and distraction.
On a side-note, you also stated that the error only happens with Postgres. That alone hints at (Django) code that deals with Postgres, IMHO. behave-django doesn't alter the handling of any specific database engine.
Thanks for the hints & suggestions : )
I had tried my best to reduce all the noise. I couldn't find a way to make it smaller than that.
The following changes in behave-django fixes the issue:
def teardown_test(self, context):
"""
Tears down the Django test
"""
context.test.tearDownClass()
context.test._post_teardown(run=True)
+ if context.test.doClassCleanups:
+ context.test.doClassCleanups()
del context.test
Does this seem legit? Tests are much slower to run now though
In hindsight, how could I have created a minimal setup to reproduce the issue?
I'm happy to create a PR if that helps shipping a fix : ) Let me know
Am I right thinking behave-django should deal with calling
doClassCleanupssomewhere then?
It shouldn't. Python's unit test framework is "disabled" for Behave to run when you invoke python manage.py behave. That's the whole point of having the BehaviorDrivenTestCase, which is meant to make the Django machinery spin up the test setup (e.g. LiveServer) but without the unittest setup and teardown, because Behave will run from the outside instead.
+ if context.test.doClassCleanups:
+ context.test.doClassCleanups()
If anything, the setupClass, addClassCleanup (and tearDownClass?) etc. should probably be disabled entirely, instead. Which should make running such a code obsolete. The test should then run faster as a consequence, not slower.
I'm happy to create a PR if that helps shipping a fix
Absolutely! But please make sure you fully understand the setup and teardown process. That might help to replace any of the current adjustments instead of just adding new code.
Right, I think I see what you mean. If I reword what you said in what I understood: behave-django intends to be as transparent as possible and wants to let Django & Behave do their thing, and just _ add _ features on the top of it. Hence why you state behave-django could ideally do even less in term of calls to setup / teardown stuff.
Another thing I hadn't realised is that behave-django has its own runner: BehaviorDrivenTestRunner (which inherits Django's DiscoverRunner)
If we agree there is an issue about doClassCleanups not being called, then I am now trying to see who's responsibility should it be to call it, and why are they not. Do you have any insight on this? I hope it makes sense for me to keep thinking the error is in between behave and behave-django.
Below is me trying to investigate further. you can see I'm slowing getting around these concepts. Thanks for the link. My hope is that at some point I'll have reached a point where you're able to pick up because you'll see more clearly than me what's happening : )
I can see that behave-django command calls exit_status = behave_main(args=behave_args) here.
The problem is that Behave at that point will never know about any Django / Unittest runner and will never call doClassCleanups. behave-django does patches Behave with monkey_patch_behave, but not enough to make it aware of the expected teardown
So, now I am leaning towards the fact that monkey_patch_behave should do more to make it aware of what the Django runner should do.
It's quite hard for me to move forward, but it's interesting. I hope the write-ups are clear enough to you
I've reached the maximum I can get to I believe. No further findings.
Forking the current repo and adding the above feature test fails CI in the same way highlighted here: https://github.com/paul-ri/behave-django/pull/1 (CI failed before the commit fixing it was pushed) . Hopefully that helps
Another thing I hadn't realised is that behave-django has its own runner:
BehaviorDrivenTestRunner(which inherits Django'sDiscoverRunner)
The problem is that Behave at that point will never know about any Django / Unittest runner and will never call
doClassCleanups.
Again: The idea of the BehaviorDrivenTestCase is to deactivate (as in "turn off") the Django test aka Python unittest setup and execution alltogether. It is by design that Behave is being called instead. That's exactly what should happen. Only the Django LiveServer is started up beforehand if desired.
The problem is that Behave at that point will never know about any Django / Unittest runner and will never call
doClassCleanups.
Again: Instead of calling doClassCleanups the addClassCleanup class method should be deactivated.
Let this sink in: What is never created doesn't need to be cleaned up.
(You literally understood the opposite of what I wrote above. No offense.)
I've reached the maximum I can get to I believe.
This is an open source project. You should assume that the maintainer of a project has good intentions and tries to direct you in the right direction. Read attentively, it will help you get further.
I apologies if my comments were read in a way that appeared I thought you were against me. It wasn't my intention. I tried to stay factual but my wording may have appeared strong ("the problem" / "never" / ...). When I said "I reached my maximum", I was stating that I can't see further avenues to explore with my current knowledge and skills with the library : )
I very much appreciate all the work you put in. I'm impressed by the project! We've been using it for years successfully :D
Thanks for trying to put me on the right direction. I am trying to take in what you say whilst also being conflicted with what I see. I want to make sure we're on the same page about what I'm seeing & what I write & what you get from my rambling : )
Again: Instead of calling doClassCleanups the addClassCleanup class method should be deactivated
Do you mean for my particular use case or that's what you aspire behave-django to do? You initially wrote:
If anything, the setupClass, addClassCleanup (and tearDownClass?) etc. should probably be disabled entirely, instead
Which made me think that's what you wanted to do in the future, not for me to do now. So I didn't act on this, thinking this would be a "nice to have in the future".
Let this sink in: What is never created doesn't need to be cleaned up. (You literally understood the opposite of what I wrote above. No offense.)
Haha, I see what you mean. I think I misunderstood the idea being I should disable these _ now _, for my current use case.
I shall investigate a bit more with that in mind.
Overall, I'm confused about which library should handle this issue. Maybe it's something specific from my project and I'll create my own runner. Maybe it's an actual problem that affects others silently.
Happy for the ticket to be closed if you see no further actions
Thanks again!
Which made me think that's what you wanted to do in the future, not for me to do now.
Overall, I'm confused about which library should handle this issue.
Happy for the ticket to be closed if you see no further actions
I appreciate that you have reported the issue. I do believe it's an actual bug that deserves to be fixed (read: in the behave-django project). As you are the first—and only one, for now—to report the problem, my impression is that it happens rarely (though, to reaffirm, I believe it's a real flaw).
To say it bluntly, I don't see the need for a life-saving emergency response from my side. I'm happy to guide you through contributing a fix that will resolve the issue. You've probably seen my comments on your own fork's PR paul-ri/behave-django#1. Please open the PR in this repository as soon as you feel it's ready for contributing.
mee too is facing the same issue of recursion depth
below error: [Previous line repeated 479 more times] RecursionError: maximum recursion depth exceeded
can some one suggest how to resolve this.
@vincyjoshy
I've not come around to look for a proper fix yet, with the help of @bittner. But I am using a wee workaround for now: Create a new runner.
from behave_django.runner import BehaviorDrivenTestRunner
class TearingDownBehaviorDrivenTestRunner(BehaviorDrivenTestRunner):
"""
Workaround for https://github.com/behave/behave-django/issues/164
Feature tests start to fail with recursion errors after a while. The cleanup steps are never called, leading up to
garbage never being cleaned up. Various errors therefore happen depending on the version of Django used.
"""
def teardown_test(self, context):
if context.test.doClassCleanups:
context.test.doClassCleanups()
super().teardown_test
Then start the tests with the --runner option. e.g.: python manage.py behave --runner "FeatureTests.runner:TearingDownBehaviorDrivenTestRunner"
I have released v1.7.0 on PyPI today. I doubt that it fixes this issue, but please update anyway to give it a try. Feedback is welcome!
I'll try to open a related PR with the changes suggested above, also considering your attempted PR paul-ri/behave-django#1. Thanks for that, Paul!
Ah great! After rebasing on 1.7.0 the added test I had, it is still failing with the recursion error: https://github.com/paul-ri/behave-django/actions/runs/16371720724
When I was looking into this issue I believe it had to do with the Scenario Outline processing, a work around is to add
import sys
sys.setrecursionlimit(20000)
in your environment.py file.
it is still failing with the recursion error
Well, it doesn't seem to. What you linked to reports that you simply have a code formatting issue. The tests all pass.
EDIT: Sorry, wrong build job. The tests in the PR still fail, correct.