Skip to content

Commit 4e4a888

Browse files
committed
Move __getattr__ None check from normalize_mark_list to get_unpacked_marks
Per reviewer feedback, move the check for module-level __getattr__ returning None to get_unpacked_marks() (the helper that fetches the mark list) instead of normalize_mark_list(). Also change from raising a TypeError to emitting a PytestCollectionWarning, so the module can still be collected normally.
1 parent 1f4930a commit 4e4a888

File tree

3 files changed

+23
-21
lines changed

3 files changed

+23
-21
lines changed

changelog/8265.improvement.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
Improved error message when a module-level ``__getattr__`` fails to raise ``AttributeError`` for missing attributes.
1+
Emit a ``PytestCollectionWarning`` when a module-level ``__getattr__`` returns ``None`` for ``pytestmark`` instead of raising ``AttributeError``.
22

3-
When a module defines a custom ``__getattr__`` that returns ``None`` instead of raising ``AttributeError``,
4-
pytest now provides a more helpful error message explaining the likely cause and suggesting the fix,
5-
rather than the cryptic "got None instead of Mark" message.
3+
Previously this caused a cryptic ``TypeError: got None instead of Mark`` error.
4+
Now pytest issues a helpful warning and continues collecting the module normally.

src/_pytest/mark/structures.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from _pytest.outcomes import fail
3131
from _pytest.raises import AbstractRaises
3232
from _pytest.scope import _ScopeName
33+
from _pytest.warning_types import PytestCollectionWarning
3334
from _pytest.warning_types import PytestUnknownMarkWarning
3435

3536

@@ -443,7 +444,18 @@ def get_unpacked_marks(
443444
mark_list.append(item)
444445
else:
445446
mark_attribute = getattr(obj, "pytestmark", [])
446-
if isinstance(mark_attribute, list):
447+
if mark_attribute is None:
448+
warnings.warn(
449+
"Module defines a `__getattr__` which returns None for "
450+
"'pytestmark' instead of raising AttributeError. "
451+
"Make sure `__getattr__` raises AttributeError for "
452+
"attributes it does not provide. "
453+
"See https://github.com/pytest-dev/pytest/issues/8265",
454+
PytestCollectionWarning,
455+
stacklevel=2,
456+
)
457+
mark_list = []
458+
elif isinstance(mark_attribute, list):
447459
mark_list = mark_attribute
448460
else:
449461
mark_list = [mark_attribute]
@@ -463,15 +475,6 @@ def normalize_mark_list(
463475
for mark in mark_list:
464476
mark_obj = getattr(mark, "mark", mark)
465477
if not isinstance(mark_obj, Mark):
466-
# Provide a helpful error message for a common mistake:
467-
# a module-level __getattr__ that doesn't raise AttributeError
468-
if mark_obj is None:
469-
raise TypeError(
470-
"got None instead of Mark - "
471-
"this is likely caused by a module-level __getattr__ "
472-
"that does not raise AttributeError for missing attributes. "
473-
"Make sure __getattr__ raises AttributeError when the attribute is not found."
474-
)
475478
raise TypeError(f"got {mark_obj!r} instead of Mark")
476479
yield mark_obj
477480

testing/test_mark.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,8 +1348,8 @@ def foo():
13481348

13491349
def test_module_getattr_without_attributeerror(pytester: Pytester) -> None:
13501350
"""
1351-
Test that a helpful error message is provided when a module-level
1352-
__getattr__ fails to raise AttributeError.
1351+
Test that a helpful warning is emitted when a module-level
1352+
__getattr__ returns None instead of raising AttributeError.
13531353
13541354
Regression test for https://github.com/pytest-dev/pytest/issues/8265
13551355
"""
@@ -1363,12 +1363,12 @@ def test_something():
13631363
assert True
13641364
"""
13651365
)
1366-
result = pytester.runpytest()
1366+
result = pytester.runpytest("-W", "always::pytest.PytestCollectionWarning")
13671367
result.stdout.fnmatch_lines(
13681368
[
1369-
"*TypeError*got None instead of Mark*",
1370-
"*module-level __getattr__*",
1371-
"*AttributeError*",
1369+
"*PytestCollectionWarning*__getattr__*returns None*AttributeError*",
13721370
]
13731371
)
1374-
assert result.ret != 0
1372+
# The module is buggy (__getattr__ returns None for all attributes),
1373+
# so no tests are collected, but pytest should NOT crash with a TypeError.
1374+
assert result.ret != ExitCode.INTERNAL_ERROR

0 commit comments

Comments
 (0)