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
« 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)."""
3import re
4from ast import literal_eval
5from contextlib import suppress
6from pathlib import Path
7from typing import (
8 List,
9 Tuple,
10)
12from vulture.core import Item
14from pytest_vulture import VultureError
15from pytest_vulture.conf.reader import IniReader
18class EntryPointFileError(VultureError):
19 """Entrypoint file error."""
21 _name = "an entry point file is missing in the setup.py/pyproject.toml file"
24class EntryPointFunctionError(VultureError):
25 """Entrypoint function error."""
27 _name = "an entry point function is missing in the setup.py/pyproject.toml file"
30_ENTRY_POINT_MSG = "\nYou can add the source_path parameter in the settings or disable with check-entry-points=0"
33class SetupManager:
34 """The setup.py parser."""
36 _entry_points: List[str]
37 _config: IniReader
38 _UNUSED_FUNCTION_MESSAGE = "unused function"
39 _PY_PROJECT = "[project.scripts]"
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)
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
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
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
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 ""
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)
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()
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
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}")
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