diff --git a/changelog/7035.trivial.rst b/changelog/7035.trivial.rst new file mode 100644 index 000000000..076cb4b4b --- /dev/null +++ b/changelog/7035.trivial.rst @@ -0,0 +1,2 @@ +The ``originalname`` attribute of ``_pytest.python.Function`` now defaults to ``name`` if not +provided explicitly, and is always set. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d3acfad44..c943824fe 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1395,11 +1395,41 @@ class Function(PyobjMixin, nodes.Item): fixtureinfo: Optional[FuncFixtureInfo] = None, originalname=None, ) -> None: + """ + param name: the full function name, including any decorations like those + added by parametrization (``my_func[my_param]``). + param parent: the parent Node. + param args: (unused) + param config: the pytest Config object + param callspec: if given, this is function has been parametrized and the callspec contains + meta information about the parametrization. + param callobj: if given, the object which will be called when the Function is invoked, + otherwise the callobj will be obtained from ``parent`` using ``originalname`` + param keywords: keywords bound to the function object for "-k" matching. + param session: the pytest Session object + param fixtureinfo: fixture information already resolved at this fixture node. + param originalname: + The attribute name to use for accessing the underlying function object. + Defaults to ``name``. Set this if name is different from the original name, + for example when it contains decorations like those added by parametrization + (``my_func[my_param]``). + """ super().__init__(name, parent, config=config, session=session) self._args = args if callobj is not NOTSET: self.obj = callobj + #: Original function name, without any decorations (for example + #: parametrization adds a ``"[...]"`` suffix to function names), used to access + #: the underlying function object from ``parent`` (in case ``callobj`` is not given + #: explicitly). + #: + #: .. versionadded:: 3.0 + self.originalname = originalname or name + + # note: when FunctionDefinition is introduced, we should change ``originalname`` + # to a readonly property that returns FunctionDefinition.name + self.keywords.update(self.obj.__dict__) self.own_markers.extend(get_unpacked_marks(self.obj)) if callspec: @@ -1434,12 +1464,6 @@ class Function(PyobjMixin, nodes.Item): self.fixturenames = fixtureinfo.names_closure self._initrequest() - #: original function name, without any decorations (for example - #: parametrization adds a ``"[...]"`` suffix to function names). - #: - #: .. versionadded:: 3.0 - self.originalname = originalname - @classmethod def from_parent(cls, parent, **kw): # todo: determine sound type limitations """ @@ -1457,11 +1481,7 @@ class Function(PyobjMixin, nodes.Item): return getimfunc(self.obj) def _getobj(self): - name = self.name - i = name.find("[") # parametrization - if i != -1: - name = name[:i] - return getattr(self.parent.obj, name) + return getattr(self.parent.obj, self.originalname) @property def _pyfuncitem(self): diff --git a/testing/python/collect.py b/testing/python/collect.py index 496a22b05..94441878c 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -6,6 +6,7 @@ import _pytest._code import pytest from _pytest.config import ExitCode from _pytest.nodes import Collector +from _pytest.pytester import Testdir class TestModule: @@ -659,16 +660,39 @@ class TestFunction: result = testdir.runpytest() result.stdout.fnmatch_lines(["* 3 passed in *"]) - def test_function_original_name(self, testdir): + def test_function_originalname(self, testdir: Testdir) -> None: items = testdir.getitems( """ import pytest + @pytest.mark.parametrize('arg', [1,2]) def test_func(arg): pass + + def test_no_param(): + pass """ ) - assert [x.originalname for x in items] == ["test_func", "test_func"] + assert [x.originalname for x in items] == [ + "test_func", + "test_func", + "test_no_param", + ] + + def test_function_with_square_brackets(self, testdir: Testdir) -> None: + """Check that functions with square brackets don't cause trouble.""" + p1 = testdir.makepyfile( + """ + locals()["test_foo[name]"] = lambda: None + """ + ) + result = testdir.runpytest("-v", str(p1)) + result.stdout.fnmatch_lines( + [ + "test_function_with_square_brackets.py::test_foo[[]name[]] PASSED *", + "*= 1 passed in *", + ] + ) class TestSorting: