Skip to content

Commit 794e6ff

Browse files
committed
Don't evaluate descriptors or import __main__.py during docstring collection
Walk __dict__ like doctest.DocTestFinder instead of inspect.getmembers(), which calls every descriptor's __get__, and skip __main__.py the way pytest's doctest plugin does. Fixes #76
1 parent 8614de6 commit 794e6ff

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

‎src/pytest_markdown_docs/plugin.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,11 @@ def find_object_tests_recursive(
357357
_visited_objects.add(id(object))
358358
docstr = inspect.getdoc(object)
359359

360-
for member_name, member in inspect.getmembers(object):
360+
# Walk __dict__ like doctest.DocTestFinder does, instead of
361+
# inspect.getmembers(), so that descriptors are never evaluated.
362+
for member in list(getattr(object, "__dict__", {}).values()):
363+
if isinstance(member, (staticmethod, classmethod)):
364+
member = member.__func__
361365
if (
362366
inspect.isclass(member)
363367
or inspect.isfunction(member)
@@ -436,6 +440,9 @@ def pytest_collect_file(
436440
if parent.config.option.markdowndocs:
437441
pathlib_path = pathlib.Path(str(file_path)) # pytest 7/8 compat
438442
if pathlib_path.suffix == ".py":
443+
if pathlib_path.name == "__main__.py":
444+
# importing it would clash with the running pytest's __main__
445+
return None
439446
return MarkdownDocstringCodeModule.from_parent(parent, path=pathlib_path)
440447
elif pathlib_path.suffix in (".md", ".mdx", ".svx"):
441448
return MarkdownTextFile.from_parent(parent, path=pathlib_path)

‎tests/plugin_test.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import re
22

3+
import pytest
4+
35
from _pytest.pytester import LineMatcher
46

57
import pytest_markdown_docs # hack: used for storing a side effect in one of the tests
@@ -1109,3 +1111,51 @@ def test_top_level_await_requires_pytest_asyncio(testdir):
11091111
result = testdir.runpytest("--markdown-docs")
11101112
result.assert_outcomes(failed=1)
11111113
result.stdout.fnmatch_lines(["*pytest-asyncio>=1.1.0*"])
1114+
1115+
1116+
def test_docstring_collection_does_not_evaluate_descriptors(testdir):
1117+
testdir.makepyfile(
1118+
"""
1119+
class Raising:
1120+
def __get__(self, instance, owner=None):
1121+
raise RuntimeError("evaluated during collection")
1122+
1123+
1124+
class Hostile:
1125+
attribute = Raising()
1126+
1127+
@staticmethod
1128+
def static():
1129+
\"\"\"
1130+
```python
1131+
assert True
1132+
```
1133+
\"\"\"
1134+
1135+
@classmethod
1136+
def klass(cls):
1137+
\"\"\"
1138+
```python
1139+
assert True
1140+
```
1141+
\"\"\"
1142+
"""
1143+
)
1144+
result = testdir.runpytest("--markdown-docs")
1145+
result.assert_outcomes(passed=2)
1146+
1147+
1148+
def test_main_py_is_skipped(testdir):
1149+
testdir.makepyfile(
1150+
__main__="""
1151+
def main():
1152+
\"\"\"
1153+
```python
1154+
assert True
1155+
```
1156+
\"\"\"
1157+
"""
1158+
)
1159+
result = testdir.runpytest("--markdown-docs")
1160+
result.assert_outcomes()
1161+
assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED

0 commit comments

Comments
 (0)