Skip to content

Commit

Permalink
Change object traversal logic
Browse files Browse the repository at this point in the history
* Prevent infinite recursion
* Prevent duplicate tests
* Include underscore-prefixed objects
  • Loading branch information
freider committed Nov 28, 2024
1 parent e486dc6 commit 7ca96e9
Showing 1 changed file with 52 additions and 30 deletions.
82 changes: 52 additions & 30 deletions src/pytest_markdown_docs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ class FenceSyntax(Enum):
superfences = "superfences"


@dataclass
@dataclass(frozen=True)
class FenceTest:
source: str
fixture_names: typing.List[str]
fixture_names: typing.Tuple[str, ...]
start_line: int


@dataclass
@dataclass(frozen=True)
class ObjectTest:
intra_object_index: int
object_name: str
Expand All @@ -53,7 +53,10 @@ class ObjectTest:

def get_docstring_start_line(obj) -> typing.Optional[int]:
# Get the source lines and the starting line number of the object
source_lines, start_line = inspect.getsourcelines(obj)
try:
source_lines, start_line = inspect.getsourcelines(obj)
except OSError:
return None

# Find the line in the source code that starts with triple quotes (""" or ''')
for idx, line in enumerate(source_lines):
Expand Down Expand Up @@ -226,9 +229,9 @@ def extract_fence_tests(
add_blank_lines = start_line - prev.count("\n")
code_block = prev + ("\n" * add_blank_lines) + block.content

fixture_names = [
fixture_names = tuple(
f[len("fixture:") :] for f in code_options if f.startswith("fixture:")
]
)
yield FenceTest(code_block, fixture_names, start_line)
prev = code_block

Expand Down Expand Up @@ -302,39 +305,58 @@ def collect(self):
)

def find_object_tests_recursive(
self, module_name: str, object: typing.Any
self,
module_name: str,
object: typing.Any,
_visited_objects: typing.Set[int] = None,
_found_tests: typing.Optional[typing.Tuple[str, int]] = None,
) -> typing.Generator[ObjectTest, None, None]:
if _visited_objects is None:
_visited_objects = set()
if _found_tests is None:
_found_tests = set()

if id(object) in _visited_objects:
return
_visited_objects.add(id(object))
docstr = inspect.getdoc(object)

if docstr:
docstring_offset = get_docstring_start_line(object)
if docstring_offset is None:
logger.warning(
"Could not find line number offset for docstring: {docstr}"
)
docstring_offset = 0

obj_name = (
getattr(object, "__qualname__", None)
or getattr(object, "__name__", None)
or "<Unnamed obj>"
)
fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
for i, fence_test in enumerate(
extract_fence_tests(docstr, docstring_offset, fence_syntax=fence_syntax)
):
yield ObjectTest(i, obj_name, fence_test)

for member_name, member in inspect.getmembers(object):
if member_name.startswith("_"):
continue

if (
inspect.isclass(member)
or inspect.isfunction(member)
or inspect.ismethod(member)
) and member.__module__ == module_name:
yield from self.find_object_tests_recursive(module_name, member)
yield from self.find_object_tests_recursive(
module_name, member, _visited_objects, _found_tests
)

if docstr:
docstring_offset = get_docstring_start_line(object)
if docstring_offset is None:
logger.warning(
f"Could not find line number offset for docstring: {docstr}"
)
else:
obj_name = (
getattr(object, "__qualname__", None)
or getattr(object, "__name__", None)
or "<Unnamed obj>"
)
fence_syntax = FenceSyntax(self.config.option.markdowndocs_syntax)
for i, fence_test in enumerate(
extract_fence_tests(
docstr, docstring_offset, fence_syntax=fence_syntax
)
):
found_test = ObjectTest(i, obj_name, fence_test)
found_test_location = (
module_name,
found_test.fence_test.start_line,
)
if found_test_location not in _found_tests:
_found_tests.add(found_test_location)
yield found_test


class MarkdownTextFile(pytest.File):
Expand Down

0 comments on commit 7ca96e9

Please sign in to comment.