diff --git a/changelog/6802.bugfix.rst b/changelog/6802.bugfix.rst new file mode 100644 index 00000000000..af9ebef3a39 --- /dev/null +++ b/changelog/6802.bugfix.rst @@ -0,0 +1 @@ +The :fixture:`testdir fixture ` works within doctests now. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c452e63c48e..d9a607db8d3 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -548,7 +548,16 @@ class Item(Node): nextitem = None - def __init__(self, name, parent=None, config=None, session=None, nodeid=None): + def __init__( + self, + name, + parent=None, + config=None, + session=None, + nodeid=None, + *, + originalname: str = None + ): super().__init__(name, parent, config, session, nodeid=nodeid) self._report_sections = [] # type: List[Tuple[str, str, str]] @@ -556,6 +565,12 @@ def __init__(self, name, parent=None, config=None, session=None, nodeid=None): #: defined properties for this test. self.user_properties = [] # type: List[Tuple[str, Any]] + #: Original function name, without any decorations (for example + #: parametrization adds a ``"[...]"`` suffix to function names). + #: + #: .. versionadded:: 3.0 + self.originalname = originalname + def runtest(self) -> None: raise NotImplementedError("runtest must be implemented by Item subclass") diff --git a/src/_pytest/pytester/__init__.py b/src/_pytest/pytester/__init__.py index 5097665bc1a..11f6007123e 100644 --- a/src/_pytest/pytester/__init__.py +++ b/src/_pytest/pytester/__init__.py @@ -599,7 +599,7 @@ def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> N self._mod_collections = ( WeakKeyDictionary() ) # type: WeakKeyDictionary[Module, List[Union[Item, Collector]]] - name = request.function.__name__ + name = self._name = request.node.originalname or request.node.name self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) # type: py.path.local """The base temporary directory. @@ -675,9 +675,8 @@ def to_text(s): return s.decode(encoding) if isinstance(s, bytes) else str(s) if lines: - funcname = self.request.function.__name__ # type: str lines = "\n".join(to_text(x) for x in lines) - items.insert(0, (funcname, lines)) + items.insert(0, (self._name, lines)) if not items: raise ValueError("no files to create") @@ -872,7 +871,7 @@ def copy_example(self, name=None): example_dir = example_dir.join(*extra_element.args) if name is None: - func_name = self.request.function.__name__ + func_name = self._name maybe_dir = example_dir / func_name maybe_file = example_dir / (func_name + ".py") @@ -1256,7 +1255,7 @@ def getmodulecol(self, source, configargs=(), withinit=False): path = self.tmpdir.join(str(source)) assert not withinit, "not supported for paths" else: - kw = {self.request.function.__name__: Source(source).strip()} + kw = {self._name: Source(source).strip()} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__="#") diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 4989d6b3984..2f0832de8e5 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1411,7 +1411,9 @@ def __init__( fixtureinfo: Optional[FuncFixtureInfo] = None, originalname=None, ) -> None: - super().__init__(name, parent, config=config, session=session) + super().__init__( + name, parent, config=config, session=session, originalname=originalname + ) self._args = args if callobj is not NOTSET: self.obj = callobj @@ -1450,12 +1452,6 @@ def __init__( 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 """ diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 1cef02cdde0..8b687999f29 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -92,6 +92,29 @@ def test_hello(testdir): result.assert_outcomes(passed=1) +def test_testdir_with_doctest(testdir: "Testdir") -> None: + """Check that testdir can be used within doctests. + + It used to use `request.function`, which is `None` with doctests.""" + testdir.makepyfile( + **{ + "sub/t-doctest.py": """ + ''' + >>> import os + >>> testdir = getfixture("testdir") + >>> str(testdir.makepyfile("content")).replace(os.sep, '/') + '.../basetemp/sub.t-doctest0/sub.py' + ''' + """, + "sub/__init__.py": "", + } + ) + result = testdir.runpytest( + "-p", "pytester", "--doctest-modules", "sub/t-doctest.py" + ) + assert result.ret == 0 + + def test_runresult_assertion_on_xfail(testdir) -> None: testdir.makepyfile( """