From 9ead96ee6c3cf7d8ec711e22503c4e6c1fca2026 Mon Sep 17 00:00:00 2001 From: symonk Date: Tue, 27 Jul 2021 21:31:56 +0100 Subject: [PATCH] #7124: Fix `__main__.py` files causing import errors with `--doctest-modules` --- changelog/7124.bugfix.rst | 1 + src/_pytest/doctest.py | 8 +++++++- testing/example_scripts/__init__.py | 0 .../example_scripts/doctest/main_py/__main__.py | 2 ++ .../doctest/main_py/test_normal_module.py | 6 ++++++ testing/test_doctest.py | 14 ++++++++++++++ 6 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 changelog/7124.bugfix.rst create mode 100644 testing/example_scripts/__init__.py create mode 100644 testing/example_scripts/doctest/main_py/__main__.py create mode 100644 testing/example_scripts/doctest/main_py/test_normal_module.py diff --git a/changelog/7124.bugfix.rst b/changelog/7124.bugfix.rst new file mode 100644 index 00000000000..191925d7a24 --- /dev/null +++ b/changelog/7124.bugfix.rst @@ -0,0 +1 @@ +Fixed an issue where ``__main__.py`` would raise an ``ImportError`` when ``--doctest-modules`` was provided. diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 870920f5a0d..f799f5f9c3e 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -125,7 +125,9 @@ def pytest_collect_file( ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config if fspath.suffix == ".py": - if config.option.doctestmodules and not _is_setup_py(fspath): + if config.option.doctestmodules and not any( + (_is_setup_py(fspath), _is_main_py(fspath)) + ): mod: DoctestModule = DoctestModule.from_parent(parent, path=fspath) return mod elif _is_doctest(config, fspath, parent): @@ -148,6 +150,10 @@ def _is_doctest(config: Config, path: Path, parent: Collector) -> bool: return any(fnmatch_ex(glob, path) for glob in globs) +def _is_main_py(path: Path) -> bool: + return path.name == "__main__.py" + + class ReprFailDoctest(TerminalRepr): def __init__( self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]] diff --git a/testing/example_scripts/__init__.py b/testing/example_scripts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/testing/example_scripts/doctest/main_py/__main__.py b/testing/example_scripts/doctest/main_py/__main__.py new file mode 100644 index 00000000000..e471d06d643 --- /dev/null +++ b/testing/example_scripts/doctest/main_py/__main__.py @@ -0,0 +1,2 @@ +def test_this_is_ignored(): + assert True diff --git a/testing/example_scripts/doctest/main_py/test_normal_module.py b/testing/example_scripts/doctest/main_py/test_normal_module.py new file mode 100644 index 00000000000..700cc9750cf --- /dev/null +++ b/testing/example_scripts/doctest/main_py/test_normal_module.py @@ -0,0 +1,6 @@ +def test_doc(): + """ + >>> 10 > 5 + True + """ + assert False diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 1d33e737830..65899c85d15 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -6,6 +6,7 @@ import pytest from _pytest.doctest import _get_checker +from _pytest.doctest import _is_main_py from _pytest.doctest import _is_mocked from _pytest.doctest import _is_setup_py from _pytest.doctest import _patch_unwrap_mock_aware @@ -811,6 +812,11 @@ def test_valid_setup_py(self, pytester: Pytester): result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 0 items*"]) + def test_main_py_does_not_cause_import_errors(self, pytester: Pytester): + p = pytester.copy_example("doctest//main_py") + result = pytester.runpytest(p, "--doctest-modules") + result.stdout.fnmatch_lines(["*collected 2 items*", "*1 failed, 1 passed*"]) + def test_invalid_setup_py(self, pytester: Pytester): """ Test to make sure that pytest reads setup.py files that are not used @@ -1518,3 +1524,11 @@ def test_is_setup_py_different_encoding(tmp_path: Path, mod: str) -> None: ) setup_py.write_bytes(contents.encode("cp1252")) assert _is_setup_py(setup_py) + + +@pytest.mark.parametrize( + "name, expected", [("__main__.py", True), ("__init__.py", False)] +) +def test_is_main_py(tmp_path: Path, name: str, expected: bool) -> None: + dunder_main = tmp_path.joinpath(name) + assert _is_main_py(dunder_main) == expected