@@ -37,14 +37,14 @@ class FenceSyntax(Enum):
3737 superfences = "superfences"
3838
3939
40- @dataclass
40+ @dataclass ( frozen = True )
4141class FenceTest :
4242 source : str
43- fixture_names : typing .List [str ]
43+ fixture_names : typing .Tuple [str , ... ]
4444 start_line : int
4545
4646
47- @dataclass
47+ @dataclass ( frozen = True )
4848class ObjectTest :
4949 intra_object_index : int
5050 object_name : str
@@ -53,7 +53,10 @@ class ObjectTest:
5353
5454def get_docstring_start_line (obj ) -> typing .Optional [int ]:
5555 # Get the source lines and the starting line number of the object
56- source_lines , start_line = inspect .getsourcelines (obj )
56+ try :
57+ source_lines , start_line = inspect .getsourcelines (obj )
58+ except OSError :
59+ return None
5760
5861 # Find the line in the source code that starts with triple quotes (""" or ''')
5962 for idx , line in enumerate (source_lines ):
@@ -226,9 +229,9 @@ def extract_fence_tests(
226229 add_blank_lines = start_line - prev .count ("\n " )
227230 code_block = prev + ("\n " * add_blank_lines ) + block .content
228231
229- fixture_names = [
232+ fixture_names = tuple (
230233 f [len ("fixture:" ) :] for f in code_options if f .startswith ("fixture:" )
231- ]
234+ )
232235 yield FenceTest (code_block , fixture_names , start_line )
233236 prev = code_block
234237
@@ -291,7 +294,9 @@ def collect(self):
291294 # but unsupported before pytest 8.1...
292295 module = import_path (self .path , root = self .config .rootpath )
293296
294- for object_test in self .find_object_tests_recursive (module .__name__ , module ):
297+ for object_test in self .find_object_tests_recursive (
298+ module .__name__ , module , set (), set ()
299+ ):
295300 fence_test = object_test .fence_test
296301 yield MarkdownInlinePythonItem .from_parent (
297302 self ,
@@ -302,39 +307,53 @@ def collect(self):
302307 )
303308
304309 def find_object_tests_recursive (
305- self , module_name : str , object : typing .Any
310+ self ,
311+ module_name : str ,
312+ object : typing .Any ,
313+ _visited_objects : typing .Set [int ],
314+ _found_tests : typing .Set [typing .Tuple [str , int ]],
306315 ) -> typing .Generator [ObjectTest , None , None ]:
316+ if id (object ) in _visited_objects :
317+ return
318+ _visited_objects .add (id (object ))
307319 docstr = inspect .getdoc (object )
308320
309- if docstr :
310- docstring_offset = get_docstring_start_line (object )
311- if docstring_offset is None :
312- logger .warning (
313- "Could not find line number offset for docstring: {docstr}"
314- )
315- docstring_offset = 0
316-
317- obj_name = (
318- getattr (object , "__qualname__" , None )
319- or getattr (object , "__name__" , None )
320- or "<Unnamed obj>"
321- )
322- fence_syntax = FenceSyntax (self .config .option .markdowndocs_syntax )
323- for i , fence_test in enumerate (
324- extract_fence_tests (docstr , docstring_offset , fence_syntax = fence_syntax )
325- ):
326- yield ObjectTest (i , obj_name , fence_test )
327-
328321 for member_name , member in inspect .getmembers (object ):
329- if member_name .startswith ("_" ):
330- continue
331-
332322 if (
333323 inspect .isclass (member )
334324 or inspect .isfunction (member )
335325 or inspect .ismethod (member )
336326 ) and member .__module__ == module_name :
337- yield from self .find_object_tests_recursive (module_name , member )
327+ yield from self .find_object_tests_recursive (
328+ module_name , member , _visited_objects , _found_tests
329+ )
330+
331+ if docstr :
332+ docstring_offset = get_docstring_start_line (object )
333+ if docstring_offset is None :
334+ logger .warning (
335+ f"Could not find line number offset for docstring: { docstr } "
336+ )
337+ else :
338+ obj_name = (
339+ getattr (object , "__qualname__" , None )
340+ or getattr (object , "__name__" , None )
341+ or "<Unnamed obj>"
342+ )
343+ fence_syntax = FenceSyntax (self .config .option .markdowndocs_syntax )
344+ for i , fence_test in enumerate (
345+ extract_fence_tests (
346+ docstr , docstring_offset , fence_syntax = fence_syntax
347+ )
348+ ):
349+ found_test = ObjectTest (i , obj_name , fence_test )
350+ found_test_location = (
351+ module_name ,
352+ found_test .fence_test .start_line ,
353+ )
354+ if found_test_location not in _found_tests :
355+ _found_tests .add (found_test_location )
356+ yield found_test
338357
339358
340359class MarkdownTextFile (pytest .File ):
0 commit comments