Coverage for src/pytest_vulture/setup_manager.py: 0.00%

111 statements  

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

1"""Manages the setup.py to get the entrypoint (and ignore then for vulture).""" 

2 

3import re 

4from ast import literal_eval 

5from contextlib import suppress 

6from pathlib import Path 

7from typing import ( 

8 List, 

9 Tuple, 

10) 

11 

12from vulture.core import Item 

13 

14from pytest_vulture import VultureError 

15from pytest_vulture.conf.reader import IniReader 

16 

17 

18class EntryPointFileError(VultureError): 

19 """Entrypoint file error.""" 

20 

21 _name = "an entry point file is missing in the setup.py/pyproject.toml file" 

22 

23 

24class EntryPointFunctionError(VultureError): 

25 """Entrypoint function error.""" 

26 

27 _name = "an entry point function is missing in the setup.py/pyproject.toml file" 

28 

29 

30_ENTRY_POINT_MSG = "\nYou can add the source_path parameter in the settings or disable with check-entry-points=0" 

31 

32 

33class SetupManager: 

34 """The setup.py parser.""" 

35 

36 _entry_points: List[str] 

37 _config: IniReader 

38 _UNUSED_FUNCTION_MESSAGE = "unused function" 

39 _PY_PROJECT = "[project.scripts]" 

40 

41 def __init__(self, config: IniReader) -> None: 

42 self._entry_points = [] 

43 self._config = config 

44 try: 

45 content = self._config.package_configuration.setup_path.read_text("utf-8") 

46 except (OSError, ValueError): 

47 return 

48 self._generate_entry_points(content) 

49 

50 def is_entry_point(self, vulture: Item) -> bool: 

51 """Check if the vulture output is an entry point 

52 Examples:: 

53 >>> config_file = Path("/tmp/test.ini") 

54 >>> config_file.write_text("[package]\\nsetup_path = /tmp/setup.py") 

55 36 

56 >>> ini = IniReader(config_file) 

57 >>> ini.read_ini() 

58 >>> Path("/tmp/setup.py").write_text("entry_points={'console_scripts': ('test=test:main', )}") 

59 54 

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

61 15 

62 >>> finder = SetupManager(ini) 

63 >>> finder.is_entry_point(Item("test", "function", Path("toto.py"), 1, 1, "unused function 'test'", 50)) 

64 False 

65 >>> finder.is_entry_point(Item("test", "function", Path("test.py"), 1, 1, "unused function 'main'", 50)) 

66 True 

67 """ 

68 for entry_point in self._entry_points: 

69 if entry_point.replace(".__init__", "") == self._python_path(vulture).replace(".__init__", ""): 

70 return True 

71 return False 

72 

73 def _python_path(self, vulture: Item) -> str: 

74 try: 

75 relative_path = vulture.filename.relative_to(self._get_dir_path().absolute()) 

76 except ValueError: 

77 relative_path = vulture.filename 

78 

79 python_path: str = relative_path.as_posix().replace("/", ".").replace(".py", "") 

80 dots_message = f"{self._UNUSED_FUNCTION_MESSAGE} '" 

81 find = re.findall(f"(?={dots_message}).*(?<=')", vulture.message) 

82 if find: 

83 function_name = find[0].replace(dots_message, "") 

84 python_path += ":" + function_name[:-1] 

85 return python_path 

86 

87 def _find_py_project_toml(self, content: str) -> str: 

88 """We do not want to add a toml dependency for now.""" 

89 entry_points = content.split(self._PY_PROJECT, 1)[1].split("[", 1)[0].split("\n") 

90 entry_points = [entry_point for entry_point in entry_points if entry_point] 

91 source_path = self._config.package_configuration.source_path 

92 root_paths = [("/", source_path.as_posix())] if source_path else [] 

93 for entry_point in entry_points: 

94 self.__parse_entry_point_line(entry_point.replace(" ", "").replace('"', ""), root_paths) 

95 return "" 

96 

97 def _generate_entry_points(self, content: str) -> None: 

98 """Parse the setup.pu file to get the entry points.""" 

99 if self._PY_PROJECT in content: 

100 self._find_py_project_toml(content) 

101 return 

102 root_paths = self.__generate_root_paths(content) 

103 entry_points = {} 

104 find = re.findall("(?=entry_points={).*(?<=})", content.replace("\n", "")) 

105 if find: 

106 element = find[0] 

107 element = element[: element.find("}") + 1] 

108 with suppress(SyntaxError): 

109 entry_points = literal_eval(element.replace("entry_points=", "")) 

110 for values in entry_points.values(): 

111 for equality in values: 

112 self.__parse_entry_point_line(equality, root_paths) 

113 

114 def __parse_entry_point_line(self, equality: str, root_paths: List[Tuple[str, str]]) -> None: 

115 """Parse an entry point value to get the entry point path.""" 

116 equality_split = equality.split("=") 

117 value = equality if len(equality_split) != 2 else equality_split[1] # noqa: PLR2004 

118 if not root_paths: 

119 self._entry_points.append(value) 

120 else: 

121 for target, destination in root_paths: 

122 if len(value) > len(target) and value[: len(target)] == target: 

123 self._entry_points.append(destination + "." + value[len(target) :]) 

124 else: # pragma: no cover 

125 self._entry_points.append(value) 

126 self.__check_entry_points() 

127 

128 def _get_dir_path(self) -> Path: 

129 source_path = self._config.package_configuration.source_path 

130 dir_path = self._config.package_configuration.setup_path.absolute().parent 

131 return dir_path / source_path 

132 

133 def __check_entry_points(self) -> None: 

134 """Check if the entry points exists.""" 

135 if not self._config.package_configuration.check_entry_points: 

136 return 

137 for entry_points in self._entry_points: 

138 try: 

139 split_points = entry_points.split(":") 

140 function_name = split_points[1] 

141 path_dots = split_points[0] 

142 except IndexError: 

143 continue 

144 dir_path = self._get_dir_path() 

145 new_path = Path(path_dots.replace(".", "/")) 

146 if (dir_path / new_path).is_dir(): # pragma: no cover 

147 path = (dir_path / new_path / "__init__.py").absolute() 

148 else: 

149 path = (dir_path / (str(new_path) + ".py")).absolute() 

150 if not path.exists(): 

151 raise EntryPointFileError(message=f"{path.as_posix()} can not be found. {_ENTRY_POINT_MSG}") 

152 with path.open() as file: 

153 content = file.read() 

154 if f"def{function_name}(" not in content.replace(" ", ""): 

155 raise EntryPointFunctionError(message=f"{entry_points} can not be found. {_ENTRY_POINT_MSG}") 

156 

157 @staticmethod 

158 def __generate_root_paths(content: str) -> List[Tuple[str, str]]: 

159 """Parse the setup.py to get the source root paths.""" 

160 root_paths: List[Tuple[str, str]] = [] 

161 package_dir = {} 

162 find = re.findall("(?=package_dir={).*(?<=})", content.replace("\n", "")) 

163 if find: 

164 element = find[0] 

165 element = element[: element.find("}") + 1] 

166 with suppress(SyntaxError): 

167 package_dir = literal_eval(element.replace("package_dir=", "")) 

168 for key, value in package_dir.items(): 

169 root_paths.append((key, value)) 

170 return root_paths