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

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

2

3 import re

4 from ast import literal_eval

5 from contextlib import suppress

6 from pathlib import Path

7 from typing import (

8 List,

9 Tuple,

10)

11

12 from vulture.core import Item

13

14 from pytest_vulture import VultureError

15 from pytest_vulture.conf.reader import IniReader

16

17

18 class EntryPointFileError(VultureError):

19 """Entrypoint file error."""

20

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

22

23

24 class 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

33 class 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