diff --git a/setup.py b/setup.py index 786033f..68e53d4 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ Plugin for py.test for doing vulture tests """ import os + from pathlib import Path from setuptools import ( @@ -44,7 +45,7 @@ setup( name='pytest-vulture', - version='2.0.2', + version='2.1.0', include_package_data=True, author='Abadie Moran', author_email='moran.abadie@gatewatcher.com', diff --git a/src/pytest_vulture/conf/package.py b/src/pytest_vulture/conf/package.py index 7b88a55..8c58bf9 100644 --- a/src/pytest_vulture/conf/package.py +++ b/src/pytest_vulture/conf/package.py @@ -23,6 +23,7 @@ class PackageConfiguration(Configuration): True """ _setup_path: Path = Path("setup.py") + _source_path: Path = Path("") _check_entry_points: bool = True _NAME = "package" @@ -30,6 +31,10 @@ class PackageConfiguration(Configuration): def setup_path(self) -> Path: return self._setup_path + @property + def source_path(self) -> Path: + return self._source_path + @property def check_entry_points(self) -> bool: return self._check_entry_points @@ -38,6 +43,8 @@ def read_ini(self, config: ConfigParser): """Read the ini file""" with suppress(KeyError): self._setup_path = Path(config[self._NAME]['setup_path']) + with suppress(KeyError): + self._source_path = Path(config[self._NAME]['source_path']) with suppress(KeyError): self._check_entry_points = self._to_bool( config[self._NAME]['check_entry_points'] diff --git a/src/pytest_vulture/setup_manager.py b/src/pytest_vulture/setup_manager.py index 6cc3301..446ede4 100644 --- a/src/pytest_vulture/setup_manager.py +++ b/src/pytest_vulture/setup_manager.py @@ -28,15 +28,16 @@ class SetupManager: _entry_points: List[str] _config: IniReader _UNUSED_FUNCTION_MESSAGE = "unused function" + _PY_PROJECT = "[project.scripts]" def __init__(self, config: IniReader): self._entry_points = [] self._config = config try: content = self._config.package_configuration.setup_path.read_text("utf-8").replace("\n", "") - except (FileNotFoundError, UnicodeDecodeError): + except (OSError, ValueError): return - self.__generate_entry_points(content) + self._generate_entry_points(content) def is_entry_point(self, vulture: Item) -> bool: """Check if the vulture output is an entry point @@ -61,23 +62,33 @@ def is_entry_point(self, vulture: Item) -> bool: return True return False - @classmethod - def _python_path(cls, vulture: Item): + def _python_path(self, vulture: Item): try: - relative_path = vulture.filename.relative_to(Path("").absolute()) + relative_path = vulture.filename.relative_to(self._get_dir_path().absolute()) except ValueError: relative_path = vulture.filename python_path = relative_path.as_posix().replace("/", ".").replace(".py", "") - dots_message = f"{cls._UNUSED_FUNCTION_MESSAGE} '" + dots_message = f"{self._UNUSED_FUNCTION_MESSAGE} '" find = re.findall(f"(?={dots_message}).*(?<=')", vulture.message) if find: function_name = find[0].replace(dots_message, "") python_path += ":" + function_name[:-1] return python_path - def __generate_entry_points(self, content: str): + def _find_py_project_toml(self, content: str): + """We do not want to add a toml dependency for now.""" + entry_points = content.split(self._PY_PROJECT, 1)[1].split("[", 1)[0].split("\n") + entry_points = [entry_point for entry_point in entry_points if entry_point] + for entry_point in entry_points: + self.__parse_entry_point_line(entry_point.replace(" ", "").replace('"', ""), []) + return "" + + def _generate_entry_points(self, content: str): """Parse the setup.pu file to get the entry points""" + if self._PY_PROJECT in content: + self._find_py_project_toml(content) + return root_paths = self.__generate_root_paths(content) entry_points = {} find = re.findall("(?=entry_points={).*(?<=})", content.replace("\n", "")) @@ -109,6 +120,11 @@ def __parse_entry_point_line(self, equality: str, root_paths: List[Tuple[str, st self._entry_points.append(value) self.__check_entry_points() + def _get_dir_path(self) -> Path: + source_path = self._config.package_configuration.source_path + dir_path = self._config.package_configuration.setup_path.absolute().parent + return dir_path / source_path + def __check_entry_points(self): """Checks if the entry points exists""" if not self._config.package_configuration.check_entry_points: @@ -120,7 +136,7 @@ def __check_entry_points(self): path_dots = split_points[0] except IndexError: continue - dir_path = self._config.package_configuration.setup_path.absolute().parent + dir_path = self._get_dir_path() new_path = Path(path_dots.replace(".", "/")) if (dir_path / new_path).is_dir(): # pragma: no cover path = (dir_path / new_path / "__init__.py").absolute() diff --git a/test/tests/functional/test_setup.py b/test/tests/functional/test_setup.py index 1707cbd..669439a 100644 --- a/test/tests/functional/test_setup.py +++ b/test/tests/functional/test_setup.py @@ -32,6 +32,7 @@ def test_is_entry_point(check_entry_points, examples_path, setup, message, file, conf = Mock() conf.package_configuration.check_entry_points = check_entry_points conf.package_configuration.setup_path = examples_path / "setups" / setup + conf.package_configuration.source_path = Path("") setup_entry_point = SetupManager(conf) vulture = Item("test", "function", Path(file), first_lineno, first_lineno, message, 50) @@ -49,6 +50,7 @@ def test_errors(examples_path, path, error): conf = Mock() conf.package_configuration.check_entry_points = True not_found_path = examples_path / "setups" / path + conf.package_configuration.source_path = Path("") conf.package_configuration.setup_path = not_found_path with pytest.raises(error): diff --git a/test/tests/unit/test_line_parsing.py b/test/tests/unit/test_line_parsing.py index 55b65f8..0571aab 100644 --- a/test/tests/unit/test_line_parsing.py +++ b/test/tests/unit/test_line_parsing.py @@ -1,6 +1,7 @@ """tests the vulture line parsing""" # pylint: disable=protected-access from pathlib import Path +from unittest.mock import Mock import pytest @@ -16,6 +17,8 @@ (Item("test", "function", Path("src/tutu.py"), 15, 15, "unused function 'tata'", 60), "src.tutu:tata"), ] ) -def test_python_path(vulture_message, path): - - assert SetupManager._python_path(vulture_message) == path +def test_python_path(vulture_message, path, tmp_path): + mock = Mock() + mock.package_configuration.source_path = Path("") + mock.package_configuration.setup_path = tmp_path + assert SetupManager(mock)._python_path(vulture_message) == path diff --git a/test/tests/unit/test_setup.py b/test/tests/unit/test_setup.py new file mode 100644 index 0000000..907c5d6 --- /dev/null +++ b/test/tests/unit/test_setup.py @@ -0,0 +1,48 @@ +"""Tests the setup parser.""" +# pylint: disable=protected-access +from pathlib import Path +from unittest.mock import Mock + +from vulture.core import Item + +from pytest_vulture.setup_manager import SetupManager + + +_DATA = """[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools", +] + +[project] +name = "test-tools" +version = "2.0.0a0" +requires-python = ">=3.9" +dynamic = [ + "dependencies", +] +[project.scripts] +test_api = "test_tools.cli.test:main" + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements/main.txt"]} +""" + +def test_py_project(tmp_path): + setup = tmp_path / "pyproject.toml" + test_tools = tmp_path / "test_tools/cli" + test_tools.mkdir(parents=True) + (test_tools / "test.py").write_text("def main()", encoding="utf-8") + setup.write_text(_DATA, encoding="utf-8") + mock = Mock() + mock.package_configuration.setup_path = setup + mock.package_configuration.source_path = tmp_path + setup = SetupManager(mock) + + assert setup._entry_points == ['test_tools.cli.test:main'] + assert setup.is_entry_point( + Item("test", "function", Path("toto.py"), 1, 1, "unused function 'main'", 50) + ) is False + assert setup.is_entry_point( + Item("test", "function", Path("test_tools/cli/test.py"), 1, 1, "unused function 'main'", 50) + ) is True