Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for warning test arguments with default values #13044

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
)


def get_default_name_val(function: Callable[..., Any]) -> dict[str, Any]:
sig = signature(function)
return {
param.name: param.default
for param in sig.parameters.values()
if param.default is not Parameter.empty
}


_non_printable_ascii_translate_table = {
i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
}
Expand Down
22 changes: 21 additions & 1 deletion src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import _PytestWrapper
from _pytest.compat import get_default_name_val
from _pytest.compat import assert_never
from _pytest.compat import get_real_func
from _pytest.compat import get_real_method
Expand Down Expand Up @@ -70,7 +71,7 @@
from _pytest.scope import _ScopeName
from _pytest.scope import HIGH_SCOPES
from _pytest.scope import Scope
from _pytest.warning_types import PytestRemovedIn9Warning
from _pytest.warning_types import PytestDefaultArgumentWarning, PytestRemovedIn9Warning
from _pytest.warning_types import PytestWarning


Expand Down Expand Up @@ -1448,6 +1449,20 @@ def deduplicate_names(*seqs: Iterable[str]) -> tuple[str, ...]:
return tuple(dict.fromkeys(name for seq in seqs for name in seq))


def default_arg_warn(nodeid: str, function_name, param_name, param_val) -> None:
msg = (
f"Test function '{function_name}' has a default argument '{param_name}={param_val}'.\n"
)
warnings.simplefilter("always", PytestDefaultArgumentWarning)
warnings.warn(PytestDefaultArgumentWarning(msg))


def check_default_arguments(func_name, default_args, nodeid):
"""Check for default arguments in the function and issue warnings."""
for arg_name, default_val in default_args.items():
default_arg_warn(nodeid, func_name, arg_name, default_val)


class FixtureManager:
"""pytest fixture definitions and information is stored and managed
from this class.
Expand Down Expand Up @@ -1528,6 +1543,11 @@ def getfixtureinfo(
ignore_args=direct_parametrize_args,
)

if func is not None:
function_name = func.__name__
default_args = get_default_name_val(func)
check_default_arguments(function_name, default_args, node.nodeid)

return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)

def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None:
Expand Down
3 changes: 2 additions & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
from _pytest.scope import _ScopeName
from _pytest.scope import Scope
from _pytest.stash import StashKey
from _pytest.warning_types import PytestCollectionWarning
from _pytest.warning_types import PytestCollectionWarning, PytestDefaultArgumentWarning, warn_explicit_for


if TYPE_CHECKING:
Expand Down Expand Up @@ -146,6 +146,7 @@ def async_fail(nodeid: str) -> None:
@hookimpl(trylast=True)
def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely convinced this is the right place for this. I wonder if something like FixtureManager.getfixtureinfo (where we already take care of reading the function's arguments) would make more sense maybe?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my most recent commit, I updated the warning checks to be in the getfixtureinfo. After looking into the function more, I also thought that it would be better to include the checks in that function.

testfunction = pyfuncitem.obj

if is_async_function(testfunction):
async_fail(pyfuncitem.nodeid)
funcargs = pyfuncitem.funcargs
Expand Down
7 changes: 7 additions & 0 deletions src/_pytest/warning_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ class PytestCollectionWarning(PytestWarning):
__module__ = "pytest"


@final
class PytestDefaultArgumentWarning(PytestWarning):
"""Warning emitted when a test function has default arguments."""

__module__ = "pytest"


class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
"""Warning class for features that will be removed in a future version."""

Expand Down
52 changes: 52 additions & 0 deletions testing/test_default_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from _pytest.pytester import Pytester

def test_no_default_argument(pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_with_default_param(param):
assert param == 42
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines([
"*fixture 'param' not found*"
])


def test_default_argument_warning(pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_with_default_param(param=42):
assert param == 42
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines([
"*PytestDefaultArgumentWarning: Test function 'test_with_default_param' has a default argument 'param=42'.*"
])


def test_no_warning_for_no_default_param(pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_without_default_param(param):
assert param is None
"""
)
result = pytester.runpytest()
assert "PytestDefaultArgumentWarning" not in result.stdout.str()


def test_warning_for_multiple_default_params(pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_with_multiple_defaults(param1=42, param2="default"):
assert param1 == 42
assert param2 == "default"
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines([
"*PytestDefaultArgumentWarning: Test function 'test_with_multiple_defaults' has a default argument 'param1=42'.*",
"*PytestDefaultArgumentWarning: Test function 'test_with_multiple_defaults' has a default argument 'param2=default'.*"
])