Coverage for src/pytest_vulture/vulture/comment.py: 0.00%

53 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-11-22 10:23 +0100

1"""Manages vulture: ignore in source code.""" 

2 

3import ast 

4from contextlib import suppress 

5from pathlib import Path 

6from typing import TYPE_CHECKING, List, Optional 

7 

8from vulture.core import Item 

9 

10 

11if TYPE_CHECKING: 

12 from _ast import AST 

13 

14 

15class CommentFinder: 

16 """Parse python code to find vulture: ignore comments.""" 

17 

18 _tree = None 

19 _content: str = "" 

20 _ignored_lines: List[int] 

21 _path: Path 

22 _VULTURE_IGNORE = "vulture: ignore" 

23 

24 def __init__(self) -> None: 

25 self._path = Path() 

26 self._ignored_lines = [] 

27 

28 def check_comment(self, vulture: Item) -> bool: 

29 """Check if the vulture output line is ignored with a # vulture: ignore comment 

30 Examples:: 

31 >>> Path("/tmp/test.py").write_text("def test():pass") 

32 15 

33 >>> finder = CommentFinder() 

34 >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 1, 1, "unused function 'test'", 50)) 

35 False 

36 >>> Path("/tmp/test.py").write_text('def test(): # vulture: ignore\\n pass') 

37 40 

38 >>> finder = CommentFinder() # the file has changed, must recreate the instance 

39 >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 1, 1, "unused function 'test'", 50)) 

40 True 

41 >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 3, 3, "unused function 'test'", 50)) 

42 False 

43 """ 

44 line_number = vulture.first_lineno 

45 if line_number is None: # pragma: no cover 

46 return False 

47 # Check if is the same file as before, if not, reload 

48 if vulture.filename.as_posix() != self._path.as_posix(): 

49 self.__reset(vulture.filename) 

50 return self.__find_rec(vulture) 

51 

52 def __find_rec(self, vulture: Item, tree: "Optional[AST]" = None, *, ignore_mode: bool = False) -> bool: 

53 """Find comments recursively.""" 

54 if tree is None: 

55 tree = self._tree 

56 if tree is None: # pragma: no cover 

57 return False 

58 with suppress(AttributeError): 

59 line_nb: int = getattr(tree, "lineno") # noqa: B009 

60 if not ignore_mode and line_nb in self._ignored_lines: 

61 ignore_mode = True 

62 if ignore_mode and vulture.first_lineno in [ 

63 line_nb, 

64 line_nb - self.__get_decorators(tree), 

65 ]: 

66 return True 

67 with suppress(AttributeError): 

68 for new_tree in tree.body: # type: ignore[attr-defined] 

69 if self.__find_rec(vulture, tree=new_tree, ignore_mode=ignore_mode): 

70 return True 

71 with suppress(AttributeError): 

72 for new_tree in tree.orelse: # type: ignore[attr-defined] 

73 if self.__find_rec(vulture, tree=new_tree, ignore_mode=ignore_mode): 

74 return True 

75 return False 

76 

77 @staticmethod 

78 def __get_decorators(tree: "Optional[AST]") -> int: 

79 if tree is None: # pragma: no cover 

80 return 0 

81 try: 

82 return len(tree.decorator_list) # type: ignore[attr-defined] 

83 except AttributeError: 

84 return 0 

85 

86 def __reset(self, path: Path) -> None: 

87 self._path = path 

88 self._ignored_lines = [] 

89 try: 

90 self._content = self._path.read_text(encoding="utf-8-sig") 

91 self._tree = ast.parse(self._content) 

92 content_split = self._content.split("\n") 

93 for index_line, elem in enumerate(content_split): 

94 if self._VULTURE_IGNORE in elem: 

95 self._ignored_lines.append(index_line + 1) 

96 

97 except OSError as err: # pragma: no cover 

98 print(f"warning : unable to read : {self._path.as_posix()} : {err}") # noqa: T201 

99 self._content = ""