Repository: Gatewatcher/pytest-vulture (language: python)

Issue:
Support pyproject.toml settings
It seems only settings in `vulture.ini` are supported. Please consider supporting vulture settings located in `pyproject.toml` of a project.

E.g.

```
[tool.vulture]
exclude = ["build/", "docs/source/conf_correct.py"]
make_whitelist = true
```


Discussion / hints:

Fix patch:
diff --git a/README.rst b/README.rst
index 7d50e49..eb32cec 100644
--- a/README.rst
+++ b/README.rst
@@ -1,79 +1,125 @@
 pytest vulture
 --------------
 
-Run vulture (https://pypi.org/project/vulture/) with pytest to find dead code.
+This plugin enables you to run `vulture` (https://pypi.org/project/vulture/) alongside `pytest`,
+allowing for dead code detection during your testing process.
 
 Sample Usage
 ============
-.. code-block:: shell
 
-   py.test --vulture
+To integrate `vulture` with `pytest` and find dead code, use the following commands:
 
-would be the most simple usage and would run vulture for all error messages.
+1. **Basic Usage**
+   Run `vulture` with `pytest` to check for dead code:
 
-.. code-block:: shell
+   .. code-block:: shell
 
-   py.test --vulture --vulture-cfg-file=/test/vulture.ini
+      pytest --vulture
 
-This would use the vulture with the /test/vulture.ini config path
+2. **Custom Configuration**
+   Specify a custom configuration file path:
 
-Ignoring vulture messages in source code
-========================================
+   .. code-block:: shell
 
-- ignoring lines :
+      pytest --vulture --vulture-cfg-file=/path/to/vulture.ini
 
-.. code-block:: python
+   **Note:** By default, the tool looks for configuration files in the following order:
 
-    def test():
-        a = 2    # vulture: ignore
+   - ``pyproject.toml``
+   - ``tox.ini``
+   - ``vulture.ini``
 
-- ignoring methods :
+Ignoring Vulture Messages
+=========================
 
-.. code-block:: python
+You can ignore specific warnings from `vulture` directly in the source code. Here’s how:
 
-    def test():  # vulture: ignore
-        pass
+- **Ignore Specific Lines:**
 
-- ignoring classes :
+  .. code-block:: python
 
-.. code-block:: python
+      def test_function():
+          unused_variable = 42  # vulture: ignore
 
-    class Test:  # vulture: ignore
-        pass
+- **Ignore Entire Methods:**
 
+  .. code-block:: python
 
-Config file
-============
+      def ignored_function():  # vulture: ignore
+          pass
+
+- **Ignore Classes:**
+
+  .. code-block:: python
+
+      class IgnoredClass:  # vulture: ignore
+          pass
+
+
+
+Configuring with ``pyproject.toml``
+===================================
+
+Here’s an example of how to configure `vulture` using ``pyproject.toml``:
+
+.. code-block:: toml
+
+    [tool.vulture]
+    # Exclude specific paths (e.g., test directories)
+    exclude = [
+        "*/test/*",
+    ]
+
+    # Ignore specific files in the `pytest` output (but they are still checked by `vulture`)
+    ignore = [
+        "src/some_ignored_file.py",
+    ]
 
-The config file (the path can be defined by the --vulture-cfg-file option) can look like this ::
+    # Ignore specific function or variable names
+    ignore-names = [
+        "deprecated_function",
+    ]
+
+    # Ignore decorators
+    ignore-decorators = [
+        "@app.route",
+        "@celery.task",
+    ]
+
+    # Ignore specific types of messages (e.g., imports)
+    ignore-types = [
+        "import",
+    ]
+
+    # Define the source path
+    source-path = "src"
+
+Configuring with ``.ini`` Config Files
+======================================
+
+Here’s an example of how to configure `vulture` using an ``.ini`` file:
+
+.. code-block:: ini
 
     [vulture]
-    # completely exclude files for vulture
     exclude =
-        */test/* # We usualy exclude tests because tests can cover dead code
+        */test/*  # Usually exclude tests as they may cover dead code
 
-    # those file are ignored by pytest, but still computed by vulture
     ignore =
-        src/toto.py
+        src/some_ignored_file.py
 
-    # ignoring names in code
     ignore-names =
-        delimiter
+        deprecated_function
 
-    # ignoring decorators
     ignore-decorators =
-      @application.errorhandler
-      @application.route
-      @celery_app.task
-      @application.app.errorhandler
+        @app.route
+        @celery.task
 
-    # ignore vulture type of messages
     ignore-types =
         attribute
         variable
 
 
-
 Acknowledgements
 ================
 
@@ -93,6 +139,11 @@ If you encounter any problems, please file an issue along with a detailed descri
 Releases
 ========
 
+2.2.0
+~~~~~~
+
+- Add pyproject.toml support for parameters
+
 2.0.2
 ~~~~~~
 
diff --git a/coverage.ini b/coverage.ini
index 6091f9c..0188d48 100644
--- a/coverage.ini
+++ b/coverage.ini
@@ -15,3 +15,4 @@ exclude_lines =
     raise AssertionError
     raise NotImplementedError
     if __name__ == (?:'|")__main__(?:'|"):
+    if TYPE_CHECKING:
diff --git a/pylint.ini b/pylint.ini
deleted file mode 100644
index cd18a26..0000000
--- a/pylint.ini
+++ /dev/null
@@ -1,175 +0,0 @@
-[MASTER]
-jobs=0
-limit-inference-results=100
-persistent=yes
-suggestion-mode=yes
-unsafe-load-any-extension=no
-
-
-[MESSAGES CONTROL]
-disable=
-    duplicate-code,
-    too-few-public-methods,
-
-
-[REPORTS]
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-output-format=text
-reports=yes
-score=yes
-
-
-[REFACTORING]
-max-nested-blocks=5
-never-returning-functions=sys.exit
-
-
-[BASIC]
-
-argument-naming-style=snake_case
-argument-rgx=[a-z_][a-z0-9_]{2,30}$
-attr-naming-style=snake_case
-attr-rgx=[a-z_][a-z0-9_]{2,50}$
-bad-names=foo,
-          bar,
-          foobar,
-          baz,
-          toto,
-          tutu,
-          tata,
-          plop,
-          lol,
-          ploplol,
-          pwet
-class-attribute-naming-style=any
-class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,50}|(__.*__))$
-class-naming-style=PascalCase
-class-rgx=[A-Z_][a-zA-Z0-9]+$
-const-naming-style=UPPER_CASE
-const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|(_logger)|(app(lication)?))$
-docstring-min-length=20
-function-naming-style=snake_case
-function-rgx=[a-z_][a-z0-9_]{2,50}$
-good-names=i,
-           j,
-           k,
-           mo,
-           np,
-           pd,
-           fd,
-           fo,
-           db,
-           _
-
-include-naming-hint=no
-inlinevar-naming-style=any
-inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
-method-naming-style=snake_case
-method-rgx=[a-z_][a-z0-9_]{2,50}$
-module-naming-style=snake_case
-module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-no-docstring-rgx=^_
-property-classes=abc.abstractproperty
-variable-naming-style=snake_case
-variable-rgx=[a-z_][a-z0-9_]{2,30}$
-
-
-[FORMAT]
-ignore-long-lines=^\s*(# )?<?https?://\S+>?$
-indent-after-paren=4
-indent-string='    '
-max-line-length=120
-max-module-lines=1000
-no-space-check=trailing-comma,
-               dict-separator
-single-line-class-stmt=no
-single-line-if-stmt=no
-
-[SPELLING]
-
-max-spelling-suggestions=4
-spelling-store-unknown-words=no
-
-
-[TYPECHECK]
-contextmanager-decorators=contextlib.contextmanager
-ignore-mixin-members=yes
-ignore-none=yes
-ignore-on-opaque-inference=yes
-ignored-classes=optparse.Values,thread._local,_thread._local
-missing-member-hint=yes
-missing-member-hint-distance=1
-missing-member-max-choices=1
-
-[LOGGING]
-logging-format-style=old
-logging-modules=logging
-
-
-[VARIABLES]
-allow-global-unused-variables=yes
-callbacks=cb_,
-          _cb
-dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
-ignored-argument-names=_.*|^ignored_|^unused_
-init-import=no
-redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
-
-
-[STRING]
-check-str-concat-over-line-jumps=no
-
-
-[MISCELLANEOUS]
-notes=FAIL,TODO,FIXME
-
-
-[SIMILARITIES]
-ignore-comments=yes
-ignore-docstrings=yes
-ignore-imports=no
-min-similarity-lines=4
-
-
-[DEPRECATED_BUILTINS]
-bad-functions=map,
-              filter
-
-
-[DESIGN]
-
-max-args=5
-max-attributes=7
-max-bool-expr=5
-max-branches=12
-max-locals=15
-max-parents=7
-max-public-methods=20
-max-returns=6
-max-statements=50
-min-public-methods=2
-
-
-[IMPORTS]
-allow-wildcard-with-all=no
-analyse-fallback-blocks=no
-deprecated-modules=optparse,tkinter.tix
-known-third-party=enchant
-
-[CLASSES]
-defining-attr-methods=__init__,
-                      __new__,
-                      setUp
-exclude-protected=_asdict,
-                  _fields,
-                  _replace,
-                  _source,
-                  _make
-
-valid-classmethod-first-arg=cls
-valid-metaclass-classmethod-first-arg=cls
-
-
-[EXCEPTIONS]
-overgeneral-exceptions=BaseException,
-                       Exception
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..6460758
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,37 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "pytest-vulture"
+version = "2.2.0"
+description = "A pytest plugin to checks dead code with vulture"
+readme = "README.rst"
+requires-python = ">=3.7"
+license = {text = "MIT"}
+authors = [
+    {name = "Abadie Moran", email = "moran.abadie@gatewatcher.com"},
+]
+maintainers = [
+    {name = "Abadie Moran", email = "moran.abadie@gatewatcher.com"},
+]
+classifiers = [
+    "Framework :: Pytest",
+]
+dependencies = [
+    "vulture>=2.0,<3.0",
+    "pytest>=7.0.0",
+]
+
+[project.entry-points.pytest11]
+vulture = "pytest_vulture.plugin"
+
+[tool.mypy]
+warn_unused_ignores = true
+strict_optional = true
+incremental = true
+ignore_missing_imports = true
+check_untyped_defs = true
+warn_no_return = true
+warn_return_any = true
+no_implicit_optional = true
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..df1872b
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,53 @@
+lint.select = ["ALL"]
+lint.unfixable = [
+    "T20", # flake8-print, would remove prints
+    "RUF001", "RUF002", "RUF003", # ambiguous-unicode-character-string, would replace characters unexpectedly
+]
+target-version = "py37"
+
+lint.ignore = [
+    "D301",
+    "D400",
+    "D105",
+    "D102",
+    "ANN002",
+    "ANN003",
+    "FA100",
+    "S603", # check for execution of untrusted input
+    "ANN101", # Missing type annotation for self in method
+    "ANN102", # Missing type annotation for cls in classmethod
+    "D205", # 1 blank line required between summary line and description
+    "FBT002", # boolean default value in function definition
+    "TRY300", #  Consider moving this statement to an `else` block
+    "COM812", # format conflits
+    "ISC001", # format conflits
+    "D107", # docstring in init
+]
+extend-exclude = [
+    "**/.tox/",
+    "**/.idea/",
+    "test/examples"
+]
+line-length = 120
+
+[lint.per-file-ignores]
+"**/test/*" = [
+    "S101", # Use of assert detected.
+    "S106", # Possible hardcoded password.
+    "B011", # Do not call assert False since python -O removes these calls.
+    "ARG001", # Unused function argument (mostly fixtures)
+    "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
+    "ANN", # flake8-annotations
+    "PLR0913", # Too many arguments to function call
+    "SLF001", # Private member accessed
+    "D103", # docstring
+]
+
+[lint.pydocstyle]
+convention = "pep257"
+
+[lint.mccabe]
+max-complexity = 10
+
+[lint.isort]
+lines-after-imports = 2
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 43ab2dd..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,40 +0,0 @@
-[aliases]
-test = pytest
-
-[egg_info]
-tag_build =
-tag_date = 0
-tag_svn_revision = 0
-
-[sdist]
-owner = root
-group = root
-formats = bztar, xztar
-
-[isort]
-known_first_party =
-not_skip =
-    __init__.py,
-multi_line_output = 3
-force_grid_wrap = 2
-combine_as_imports = true
-combine_star = true
-include_trailing_comma = true
-lines_after_imports = 2
-lines_between_types = 1
-
-[pycodestyle]
-; E501 refers to line too long errors, which is already handled by pylint
-ignore = E501
-
-[mypy]
-warn_unused_ignores = true
-strict_optional = true
-cache_dir = .cache/mypy
-incremental = true
-ignore_missing_imports = true
-check_untyped_defs = true
-show_none_errors = true
-warn_no_return = true
-warn_return_any = true
-no_implicit_optional = true
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 0b167ba..0000000
--- a/setup.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# -*- coding: utf-8 -*-
-"""pytest-vulture
-=============
-Plugin for py.test for doing vulture tests
-"""
-import os
-
-from pathlib import Path
-
-from setuptools import (
-    find_packages,
-    setup,
-)
-
-
-IS_TEST = bool(os.environ.get("IS_UNIT_TEST", 0))
-
-install_requires = [
-    'vulture <3.0, >2.0 ',
-]
-if not IS_TEST:
-    install_requires.append("pytest >= 7.0.0")
-
-test_requires = [
-    'pylint==2.14.5',
-    'pytest==7.1.2',
-    'pytest-runner==5.2',
-    'pytest-cov==2.10.1',
-    'pytest-pycodestyle==2.3.0',
-    'pytest-pylint==0.18.0',
-    'pytest-isort==3.0.0',
-    'pytest-mccabe==2.0',
-    'pytest-mypy==0.9.1',
-]
-
-dev_requires = [
-]
-
-# to prevent coverage bugs
-ENTRY_POINTS = {
-    "pytest11": [
-        "vulture = pytest_vulture.plugin",
-    ]
-} if not IS_TEST else {}
-
-setup(
-    name='pytest-vulture',
-    version='2.1.1',
-    include_package_data=True,
-    author='Abadie Moran',
-    author_email='moran.abadie@gatewatcher.com',
-    maintainer='Abadie Moran',
-    maintainer_email='moran.abadie@gatewatcher.com',
-    license='MIT',
-    url='https://github.com/Gatewatcher/pytest-vulture',
-    description='A pytest plugin to checks dead code with vulture',
-    long_description=(Path(__file__).parent / "README.rst").read_text(encoding="utf-8"),
-    long_description_content_type="text/x-rst",
-    package_dir={
-        '': 'src',
-    },
-    packages=find_packages(
-        'src'
-    ),
-    install_requires=install_requires,
-    tests_require=test_requires,
-    extras_require={
-        'test': test_requires,
-        'dev': test_requires + dev_requires,
-    },
-    entry_points=ENTRY_POINTS,
-    classifiers=["Framework :: Pytest"],
-)
diff --git a/tox.ini b/tox.ini
index 1000d13..3e76477 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,7 @@
 [tox]
-envlist = py3{7,8,9,10}
+envlist = py3{7,8,9,10,11,12}
 distdir = {toxinidir}/dist
+skipsdist = True
 
 [pytest]
 testpaths = test/tests/ src/
@@ -20,15 +21,17 @@ addopts =
     --cov src/
     --cov test/tests/
     --doctest-modules
-    --isort
-    --mccabe
     --mypy
-    --pylint --pylint-rcfile pylint.ini
     --disable-pytest-warnings
     --verbose
 
 
 [testenv]
-deps = .[test]
-setenv = IS_UNIT_TEST = 1
-commands = {envpython} setup.py test --addopts "--basetemp {envtmpdir} --confcutdir .. --cache-clear {posargs}"
+deps =
+    vulture>=2.0,<3.0
+    -r test-requirements.txt
+commands = pytest {posargs}
+
+[package]
+setup_path = pyproject.toml
+source_path = src


Test patch:
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..e069b8f
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,15 @@
+[pytest]
+testpaths =
+    src/
+    test/tests
+pythonpath = src
+log_format =
+    %(filename)s:%(lineno)d: [%(name)s:%(levelname)s] %(asctime)s: %(message)s
+log_date_format = %Y-%m-%d %H:%M:%S
+addopts =
+    --cov-config coverage.ini
+    --cov-report term
+    --cov-report html:coverage/html
+    --cov src/
+    --doctest-modules
+    --mypy
diff --git a/src/pytest_vulture/__init__.py b/src/pytest_vulture/__init__.py
index 7785fab..fec55ab 100644
--- a/src/pytest_vulture/__init__.py
+++ b/src/pytest_vulture/__init__.py
@@ -1,21 +1,22 @@
-"""The vulture pytest plugin code source"""
+"""The vulture pytest plugin code source."""
 
 __all__ = ["VultureError"]
 
 
 class VultureError(Exception):
-    """Any pytest vulture exception"""
+    """Any pytest vulture exception."""
+
     __message: str
     _name = "vulture exception"
 
-    def __init__(self, message: str = "An error occurred"):
+    def __init__(self, message: str = "An error occurred") -> None:
         super().__init__(self)
         self.__message = message
 
     @property
     def message(self) -> str:  # pragma: no cover
-        """Get the error message if it has one"""
+        """Get the error message if it has one."""
         return self.__message
 
-    def __str__(self):  # pragma: no cover
+    def __str__(self) -> str:  # pragma: no cover
         return f"{self._name} : {self.message}"
diff --git a/src/pytest_vulture/conf/__init__.py b/src/pytest_vulture/conf/__init__.py
index b01327d..7ab7632 100644
--- a/src/pytest_vulture/conf/__init__.py
+++ b/src/pytest_vulture/conf/__init__.py
@@ -1,1 +1,1 @@
-"""Manages pytest vulture config (set with --vulture-cfg-file)"""
+"""Manages pytest vulture config (set with --vulture-cfg-file)."""
diff --git a/src/pytest_vulture/conf/base.py b/src/pytest_vulture/conf/base.py
index cde3633..3af57e3 100644
--- a/src/pytest_vulture/conf/base.py
+++ b/src/pytest_vulture/conf/base.py
@@ -1,32 +1,45 @@
-"""A pytest vulture configuration"""
-import re
+"""A pytest vulture configuration."""
 
+import re
 from abc import (
     ABC,
     abstractmethod,
 )
 from configparser import ConfigParser
-from typing import List
+from typing import ClassVar, List
 
 
 class Configuration(ABC):
-    """The abstract class of a pytest vulture configuration"""
-    _TRUE_VALUES = ["true", "True", "1", "yes"]
+    """The abstract class of a pytest vulture configuration."""
+
+    _TRUE_VALUES: ClassVar[List[str]] = ["true", "True", "1", "yes"]
+    _is_pyproject: bool
+
+    def __init__(self, *, is_pyproject: bool) -> None:
+        self._is_pyproject = is_pyproject
 
     @abstractmethod
-    def read_ini(self, config: ConfigParser) -> bool:
-        """Read the ini file"""
+    def read_ini(self, config: ConfigParser) -> None:
+        """Read the ini file."""
+
+    @abstractmethod
+    def read_tomli(self, data: dict) -> None:
+        """Read a toml file."""
 
     @staticmethod
     def _to_list(config_string: str) -> List[str]:
         """Convert a string config element to a list
-           Examples::
-                >>> Configuration._to_list(" */test/*\\n */test_2/*")
-                ['*/test/*', '*/test_2/*']
+        Examples::
+             >>> Configuration._to_list(" */test/*\\n */test_2/*")
+             ['*/test/*', '*/test_2/*']
         """
-        list_elements = re.split('[,\n]', config_string)
+        list_elements = re.split("[,\n]", config_string)
         return [element.lstrip().rstrip() for element in list_elements if element.lstrip().rstrip()]
 
     @classmethod
     def _to_bool(cls, config_string: str) -> bool:
         return config_string in cls._TRUE_VALUES
+
+    def _get_parameters(self, data: dict) -> dict:
+        answer = data.get("tool", {}).get("vulture", {})
+        return answer if isinstance(answer, dict) else {}
diff --git a/src/pytest_vulture/conf/package.py b/src/pytest_vulture/conf/package.py
index 8c58bf9..b3cc243 100644
--- a/src/pytest_vulture/conf/package.py
+++ b/src/pytest_vulture/conf/package.py
@@ -1,4 +1,5 @@
-"""The package configuration"""
+"""The package configuration."""
+
 from configparser import ConfigParser
 from contextlib import suppress
 from pathlib import Path
@@ -15,15 +16,16 @@ class PackageConfiguration(Configuration):
         >>> config = ConfigParser()
         >>> config.read(Path(config_file))
         ['/tmp/test.ini']
-        >>> package_config = PackageConfiguration()
+        >>> package_config = PackageConfiguration(is_pyproject=False)
         >>> package_config.read_ini(config)
         >>> package_config.setup_path
         PosixPath('setup.py')
         >>> package_config.check_entry_points
         True
     """
+
     _setup_path: Path = Path("setup.py")
-    _source_path: Path = Path("")
+    _source_path: Path = Path()
     _check_entry_points: bool = True
     _NAME = "package"
 
@@ -31,6 +33,10 @@ class PackageConfiguration(Configuration):
     def setup_path(self) -> Path:
         return self._setup_path
 
+    @setup_path.setter
+    def setup_path(self, setup_path: Path) -> None:
+        self._setup_path = setup_path
+
     @property
     def source_path(self) -> Path:
         return self._source_path
@@ -39,13 +45,22 @@ def source_path(self) -> Path:
     def check_entry_points(self) -> bool:
         return self._check_entry_points
 
-    def read_ini(self, config: ConfigParser):
-        """Read the ini file"""
+    def read_tomli(self, data: dict) -> None:
+        parameters = self._get_parameters(data)
+        if not parameters:  # pragma: no cover
+            return
+
+        str_source = parameters.get("source-path")
+        self._source_path = Path(str_source) if str_source else Path()
+        self._check_entry_points = data.get("check-entry-points", True)
+
+    def read_ini(self, config: ConfigParser) -> None:
+        """Read the ini file."""
+        if self._is_pyproject:  # pragma: no cover
+            return
         with suppress(KeyError):
-            self._setup_path = Path(config[self._NAME]['setup_path'])
+            self._setup_path = Path(config[self._NAME]["setup_path"])
         with suppress(KeyError):
-            self._source_path = Path(config[self._NAME]['source_path'])
+            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']
-            )
+            self._check_entry_points = self._to_bool(config[self._NAME]["check_entry_points"])
diff --git a/src/pytest_vulture/conf/reader.py b/src/pytest_vulture/conf/reader.py
index 55c7601..922fc89 100644
--- a/src/pytest_vulture/conf/reader.py
+++ b/src/pytest_vulture/conf/reader.py
@@ -1,32 +1,44 @@
-"""The full configuration manager"""
+"""The full configuration manager."""
+
+import sys
 from configparser import ConfigParser
 from pathlib import Path
 
+
+if sys.version_info >= (3, 11):  # pragma: no cover
+    import tomllib
+else:  # pragma: no cover
+    import tomli as tomllib
+
 from pytest_vulture.conf.package import PackageConfiguration
 from pytest_vulture.conf.vulture import VultureConfiguration
 
 
 class IniReader:
     """The full configuration parser
-     Examples::
-        >>> config_file = Path("/tmp/test.ini")
-        >>> config_file.write_text("[package]\\nsetup_path = custom.py\\n[vulture]\\nignore-names = test")
-        62
-        >>> package_config = IniReader(Path("/tmp/test.ini"))
-        >>> package_config.read_ini()
-        >>> package_config.package_configuration.setup_path
-        PosixPath('custom.py')
-        >>> package_config.vulture_configuration.ignore_names
-        ['test']
+    Examples::
+       >>> config_file = Path("/tmp/test.ini")
+       >>> config_file.write_text("[package]\\nsetup_path = custom.py\\n[vulture]\\nignore-names = test")
+       62
+       >>> package_config = IniReader(Path("/tmp/test.ini"))
+       >>> package_config.read_ini()
+       >>> package_config.package_configuration.setup_path
+       PosixPath('custom.py')
+       >>> package_config.vulture_configuration.ignore_names
+       ['test']
     """
+
+    _is_pyproject: bool
+
     _vulture_configuration: VultureConfiguration
     _package_configuration: PackageConfiguration
     _path_ini: Path
 
-    def __init__(self, path_ini: Path = Path("vulture.ini")):
+    def __init__(self, path_ini: Path = Path("vulture.ini"), *, is_pyproject: bool = False) -> None:
         self._path_ini = path_ini
-        self._vulture_configuration = VultureConfiguration()
-        self._package_configuration = PackageConfiguration()
+        self._vulture_configuration = VultureConfiguration(is_pyproject=is_pyproject)
+        self._package_configuration = PackageConfiguration(is_pyproject=is_pyproject)
+        self._is_pyproject = is_pyproject
 
     @property
     def package_configuration(self) -> PackageConfiguration:
@@ -36,10 +48,18 @@ def package_configuration(self) -> PackageConfiguration:
     def vulture_configuration(self) -> VultureConfiguration:
         return self._vulture_configuration
 
-    def read_ini(self):
-        """Read and parse the ini file"""
+    def read_ini(self) -> None:
+        """Read and parse the ini file."""
         if self._path_ini.is_file():
-            config = ConfigParser()
-            config.read(self._path_ini)
-            self._vulture_configuration.read_ini(config)
-            self._package_configuration.read_ini(config)
+            if self._is_pyproject:
+                with self._path_ini.open("rb") as file:
+                    pyproject_data = tomllib.load(file)
+                self._vulture_configuration.read_tomli(pyproject_data)
+                self._package_configuration.setup_path = self._path_ini
+                self._package_configuration.read_tomli(pyproject_data)
+
+            else:
+                config = ConfigParser()
+                config.read(self._path_ini)
+                self._vulture_configuration.read_ini(config)
+                self._package_configuration.read_ini(config)
diff --git a/src/pytest_vulture/conf/vulture.py b/src/pytest_vulture/conf/vulture.py
index ac6174f..a42bd8a 100644
--- a/src/pytest_vulture/conf/vulture.py
+++ b/src/pytest_vulture/conf/vulture.py
@@ -1,7 +1,8 @@
-"""The client pytest vulture configuration"""
+"""The client pytest vulture configuration."""
+
 from configparser import ConfigParser
 from contextlib import suppress
-from typing import List
+from typing import ClassVar, List
 
 from pytest_vulture.conf.base import Configuration
 
@@ -16,23 +17,29 @@ class VultureConfiguration(Configuration):
         >>> config = ConfigParser()
         >>> config.read(Path(config_file))
         ['/tmp/test.ini']
-        >>> package_config = VultureConfiguration()
+        >>> package_config = VultureConfiguration(is_pyproject=False)
         >>> package_config.read_ini(config)
         >>> package_config.ignore_decorators
         ['@test']
     """
-    EXCLUDE_DEFAULT: List[str] = [
-        "*/.tox/*", "*/.git/*", "*/.cache/*", "*/.idea/*", "*/.eggs/*"
+
+    EXCLUDE_DEFAULT: ClassVar[List[str]] = [
+        "*/.tox/*",
+        "*/.git/*",
+        "*/.cache/*",
+        "*/.idea/*",
+        "*/.eggs/*",
     ]
     _exclude: List[str] = EXCLUDE_DEFAULT
-    _CONFIG_NAME: str = "vulture"
+    CONFIG_NAME: str = "vulture"
+    CONFIG_PYPROJECT_NAME: str = "tool.vulture"
     _ignore_names: List[str]
     _ignore_decorators: List[str]
     _ignore: List[str]
     _ignore_types: List[str]
 
-    def __init__(self):
-        super().__init__()
+    def __init__(self, *, is_pyproject: bool) -> None:
+        super().__init__(is_pyproject=is_pyproject)
         self._ignore_names: List[str] = []
         self._ignore_decorators: List[str] = []
         self._ignore: List[str] = []
@@ -58,31 +65,32 @@ def ignore_names(self) -> List[str]:
     def exclude(self) -> List[str]:
         return self._exclude
 
-    def read_ini(self, config: ConfigParser):
-        """Read the ini file"""
+    def read_tomli(self, data: dict) -> None:
+        parameters = self._get_parameters(data)
+        if not parameters:  # pragma: no cover
+            return
+
+        self._exclude = self.EXCLUDE_DEFAULT + parameters.get("exclude", [])
+        self._ignore_names = parameters.get("ignore-names", [])
+        self._ignore_decorators = parameters.get("ignore-decorators", [])
+        self._ignore = parameters.get("ignore", [])
+        self._ignore_types = parameters.get("ignore-types", [])
+
+    def read_ini(self, config: ConfigParser) -> None:
+        """Read the ini file."""
         with suppress(KeyError):
-            self._exclude = self.EXCLUDE_DEFAULT + self._to_list(
-                config[self._CONFIG_NAME]['exclude']
-            )
+            self._exclude = self.EXCLUDE_DEFAULT + self._to_list(config[self.CONFIG_NAME]["exclude"])
 
         self.__read_ignore(config)
 
-    def __read_ignore(self, config: ConfigParser):
+    def __read_ignore(self, config: ConfigParser) -> None:
         with suppress(KeyError):
-            self._ignore_names = self._to_list(
-                config[self._CONFIG_NAME]['ignore-names']
-            )
+            self._ignore_names = self._to_list(config[self.CONFIG_NAME]["ignore-names"])
         with suppress(KeyError):
-            self._ignore_decorators = self._to_list(
-                config[self._CONFIG_NAME]['ignore-decorators']
-            )
+            self._ignore_decorators = self._to_list(config[self.CONFIG_NAME]["ignore-decorators"])
 
         with suppress(KeyError):
-            self._ignore = self._to_list(
-                config[self._CONFIG_NAME]['ignore']
-            )
+            self._ignore = self._to_list(config[self.CONFIG_NAME]["ignore"])
 
         with suppress(KeyError):
-            self._ignore_types = self._to_list(
-                config[self._CONFIG_NAME]['ignore-types']
-            )
+            self._ignore_types = self._to_list(config[self.CONFIG_NAME]["ignore-types"])
diff --git a/src/pytest_vulture/plugin.py b/src/pytest_vulture/plugin.py
index 8f302aa..a7fef93 100644
--- a/src/pytest_vulture/plugin.py
+++ b/src/pytest_vulture/plugin.py
@@ -1,75 +1,102 @@
-# -*- coding: utf-8 -*-
-"""Pytest vulture plugins"""
-# pylint: disable=subclassed-final-class
+"""Pytest vulture plugins."""
+
+from contextlib import suppress
 from pathlib import Path
+from typing import TYPE_CHECKING, Iterable, Optional, Tuple, Union
 
 import pytest
-
 from _pytest.main import Session
 
 from pytest_vulture import VultureError
 from pytest_vulture.conf.reader import IniReader
+from pytest_vulture.conf.vulture import VultureConfiguration
 from pytest_vulture.vulture.manager import VultureManager
 
 
-def pytest_addoption(parser):
-    """Add all the pytest vulture command line options"""
+if TYPE_CHECKING:
+    from _pytest.config.argparsing import Parser
+    from _pytest.nodes import Collector, Item
+    from _pytest.python import Package
+    from py._code.code import ExceptionInfo, TerminalRepr
+
+
+def pytest_addoption(parser: "Parser") -> None:
+    """Add all the pytest vulture command line options."""
     group = parser.getgroup("general")
     # Set the --vulture option in the setup.py
+    group.addoption("--vulture", action="store_true", help="run vulture")
     group.addoption(
-        '--vulture', action='store_true', help="run vulture"
-    )
-    group.addoption(
-        '--vulture-cfg-file', help="Defines the vulture config file path",
-        default="tox.ini",
+        "--vulture-cfg-file",
+        help="Defines the vulture config file path",
+        default="",
     )
 
 
-def pytest_sessionstart(session):
+def pytest_sessionstart(session: "_VultureSession") -> None:
     """Call vulture only at the start, before entering the run test loop."""
     config = session.config
     if config.option.vulture:
-        root_dir = session.startdir
-        reader = IniReader(path_ini=Path(config.option.vulture_cfg_file))
+        root_dir: Path = session.startdir
+        vulture_cfg_file = config.option.vulture_cfg_file
+        config_path, is_py = (Path(vulture_cfg_file), False) if vulture_cfg_file else _find_cfg_path()
+        reader = IniReader(path_ini=config_path, is_pyproject=is_py)
         reader.read_ini()
         session.vulture = VultureManager(root_dir, reader)
         session.vulture.call()
 
 
-def pytest_collect_file(file_path, parent):
-    """Search and apply the vulture result (run at the session start) on the python file"""
+def pytest_collect_file(file_path: Path, parent: "Package") -> "Optional[VulturePinningFile]":
+    """Search and apply the vulture result (run at the session start) on the python file."""
     config = parent.config
-    if config.option.vulture and file_path.suffix == '.py':
-        return VulturePinningFile.from_parent(parent=parent, path=file_path)
+    if config.option.vulture and file_path.suffix == ".py":
+        answer: VulturePinningFile = VulturePinningFile.from_parent(parent=parent, path=file_path)
+        return answer
     return None
 
 
-class _VultureSession(Session):  # type: ignore
+class _VultureSession(Session):  # type: ignore[misc]
     vulture: VultureManager
+    startdir: Path
 
 
 class VultureItem(pytest.Item):
     """vulture pytest running class."""
+
     session: _VultureSession
 
-    def runtest(self):
+    def runtest(self) -> None:
         """Raise an error if a vulture message occurred in the tested file."""
-        error_message = self.session.vulture.get_file_errors(self.fspath)
+        error_message = self.session.vulture.get_file_errors(str(self.fspath))
         if error_message:
             raise VultureError(message=error_message)
 
-    def repr_failure(self, excinfo, *args, **kwargs):  # pylint: disable=signature-differs
+    def repr_failure(self, excinfo: "ExceptionInfo", *args, **kwargs) -> "Union[str, TerminalRepr]":
         """If VultureError is raised, it means that a vulture message occurred in the tested file."""
         if excinfo.errisinstance(VultureError):
-            return excinfo.value.message
+            answer: str = excinfo.value.message
+            return answer
         return super().repr_failure(excinfo, *args, **kwargs)
 
-    def reportinfo(self):
-        """Generate the vulture test report"""
+    def reportinfo(self) -> "Tuple[Path, None, str]":
+        """Generate the vulture test report."""
         return self.path, None, f"[vulture] {self.path}"
 
+
 class VulturePinningFile(pytest.File):
-    """File that is tested by pytest"""
+    """File that is tested by pytest."""
 
-    def collect(self):
+    def collect(self) -> "Iterable[Union[Item, Collector]]":
+        """Probably useless."""
         yield VultureItem.from_parent(parent=self, name="vulture")
+
+
+def _find_cfg_path() -> Tuple[Path, bool]:
+    with suppress(OSError):
+        if VultureConfiguration.CONFIG_PYPROJECT_NAME in Path("pyproject.toml").read_text():
+            return Path("pyproject.toml"), True
+
+        for path_str in ("tox.ini", "vulture.ini"):
+            path = Path(path_str)
+            if path.is_file() and VultureConfiguration.CONFIG_NAME in path.read_text():
+                return Path(path_str), False
+    return Path("tox.ini"), False
diff --git a/src/pytest_vulture/py.typed b/src/pytest_vulture/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/src/pytest_vulture/setup_manager.py b/src/pytest_vulture/setup_manager.py
index 23f5dd6..58b2d1e 100644
--- a/src/pytest_vulture/setup_manager.py
+++ b/src/pytest_vulture/setup_manager.py
@@ -1,7 +1,7 @@
-"""Manages the setup.py to get the entrypoint (and ignore then for vulture)"""
-# pylint: disable=eval-used
-import re
+"""Manages the setup.py to get the entrypoint (and ignore then for vulture)."""
 
+import re
+from ast import literal_eval
 from contextlib import suppress
 from pathlib import Path
 from typing import (
@@ -16,21 +16,29 @@
 
 
 class EntryPointFileError(VultureError):
-    _name = "an entry point file is missing in the setup.py file"
+    """Entrypoint file error."""
+
+    _name = "an entry point file is missing in the setup.py/pyproject.toml file"
 
 
 class EntryPointFunctionError(VultureError):
-    _name = "an entry point function is missing in the setup.py file"
+    """Entrypoint function error."""
+
+    _name = "an entry point function is missing in the setup.py/pyproject.toml file"
+
+
+_ENTRY_POINT_MSG = "\nYou can add the source_path parameter in the settings or disable with check-entry-points=0"
 
 
 class SetupManager:
-    """The setup.py parser"""
+    """The setup.py parser."""
+
     _entry_points: List[str]
     _config: IniReader
     _UNUSED_FUNCTION_MESSAGE = "unused function"
     _PY_PROJECT = "[project.scripts]"
 
-    def __init__(self, config: IniReader):
+    def __init__(self, config: IniReader) -> None:
         self._entry_points = []
         self._config = config
         try:
@@ -62,13 +70,13 @@ def is_entry_point(self, vulture: Item) -> bool:
                 return True
         return False
 
-    def _python_path(self, vulture: Item):
+    def _python_path(self, vulture: Item) -> str:
         try:
             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", "")
+        python_path: str = relative_path.as_posix().replace("/", ".").replace(".py", "")
         dots_message = f"{self._UNUSED_FUNCTION_MESSAGE} '"
         find = re.findall(f"(?={dots_message}).*(?<=')", vulture.message)
         if find:
@@ -76,16 +84,18 @@ def _python_path(self, vulture: Item):
             python_path += ":" + function_name[:-1]
         return python_path
 
-    def _find_py_project_toml(self, content: str):
+    def _find_py_project_toml(self, content: str) -> 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]
+        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]
+        source_path = self._config.package_configuration.source_path
+        root_paths = [("/", source_path.as_posix())] if source_path else []
         for entry_point in entry_points:
-            self.__parse_entry_point_line(entry_point.replace(" ", "").replace('"', ""), [])
+            self.__parse_entry_point_line(entry_point.replace(" ", "").replace('"', ""), root_paths)
         return ""
 
-    def _generate_entry_points(self, content: str):
-        """Parse the setup.pu file to get the entry points"""
+    def _generate_entry_points(self, content: str) -> None:
+        """Parse the setup.pu file to get the entry points."""
         if self._PY_PROJECT in content:
             self._find_py_project_toml(content)
             return
@@ -94,28 +104,23 @@ def _generate_entry_points(self, content: str):
         find = re.findall("(?=entry_points={).*(?<=})", content.replace("\n", ""))
         if find:
             element = find[0]
-            element = element[:element.find("}") + 1]
+            element = element[: element.find("}") + 1]
             with suppress(SyntaxError):
-                entry_points = eval(element.replace("entry_points=", ""))
+                entry_points = literal_eval(element.replace("entry_points=", ""))
         for values in entry_points.values():
             for equality in values:
                 self.__parse_entry_point_line(equality, root_paths)
 
-    def __parse_entry_point_line(self, equality: str, root_paths: List[Tuple[str, str]]):
-        """Parse an entry point value to get the entry point path"""
+    def __parse_entry_point_line(self, equality: str, root_paths: List[Tuple[str, str]]) -> None:
+        """Parse an entry point value to get the entry point path."""
         equality_split = equality.split("=")
-        if len(equality_split) != 2:
-            value = equality
-        else:
-            value = equality_split[1]
+        value = equality if len(equality_split) != 2 else equality_split[1]  # noqa: PLR2004
         if not root_paths:
             self._entry_points.append(value)
         else:
             for target, destination in root_paths:
-                if len(value) > len(target) and value[:len(target)] == target:
-                    self._entry_points.append(
-                        destination + "." + value[len(target):]
-                    )
+                if len(value) > len(target) and value[: len(target)] == target:
+                    self._entry_points.append(destination + "." + value[len(target) :])
                 else:  # pragma: no cover
                     self._entry_points.append(value)
         self.__check_entry_points()
@@ -125,8 +130,8 @@ def _get_dir_path(self) -> 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"""
+    def __check_entry_points(self) -> None:
+        """Check if the entry points exists."""
         if not self._config.package_configuration.check_entry_points:
             return
         for entry_points in self._entry_points:
@@ -143,27 +148,23 @@ def __check_entry_points(self):
             else:
                 path = (dir_path / (str(new_path) + ".py")).absolute()
             if not path.exists():
-                raise EntryPointFileError(
-                    message=f"{path.as_posix()} can not be found."
-                )
+                raise EntryPointFileError(message=f"{path.as_posix()} can not be found. {_ENTRY_POINT_MSG}")
             with path.open() as file:
                 content = file.read()
                 if f"def{function_name}(" not in content.replace(" ", ""):
-                    raise EntryPointFunctionError(
-                        message=f"{entry_points} can not be found."
-                    )
+                    raise EntryPointFunctionError(message=f"{entry_points} can not be found. {_ENTRY_POINT_MSG}")
 
     @staticmethod
     def __generate_root_paths(content: str) -> List[Tuple[str, str]]:
-        """Parse the setup.py to get the source root paths"""
+        """Parse the setup.py to get the source root paths."""
         root_paths: List[Tuple[str, str]] = []
         package_dir = {}
         find = re.findall("(?=package_dir={).*(?<=})", content.replace("\n", ""))
         if find:
             element = find[0]
-            element = element[:element.find("}") + 1]
+            element = element[: element.find("}") + 1]
             with suppress(SyntaxError):
-                package_dir = eval(element.replace("package_dir=", ""))
+                package_dir = literal_eval(element.replace("package_dir=", ""))
         for key, value in package_dir.items():
             root_paths.append((key, value))
         return root_paths
diff --git a/src/pytest_vulture/vulture/__init__.py b/src/pytest_vulture/vulture/__init__.py
index d7d84b2..e6044ea 100644
--- a/src/pytest_vulture/vulture/__init__.py
+++ b/src/pytest_vulture/vulture/__init__.py
@@ -1,1 +1,1 @@
-"""Manages vulture"""
+"""Manages vulture."""
diff --git a/src/pytest_vulture/vulture/comment.py b/src/pytest_vulture/vulture/comment.py
index 843124c..41bc069 100644
--- a/src/pytest_vulture/vulture/comment.py
+++ b/src/pytest_vulture/vulture/comment.py
@@ -1,40 +1,45 @@
-"""Manages vulture: ignore in source code"""
-import ast
+"""Manages vulture: ignore in source code."""
 
+import ast
 from contextlib import suppress
 from pathlib import Path
-from typing import List
+from typing import TYPE_CHECKING, List, Optional
 
 from vulture.core import Item
 
 
+if TYPE_CHECKING:
+    from _ast import AST
+
+
 class CommentFinder:
-    """Parse python code to find vulture: ignore comments"""
+    """Parse python code to find vulture: ignore comments."""
+
     _tree = None
     _content: str = ""
     _ignored_lines: List[int]
     _path: Path
     _VULTURE_IGNORE = "vulture: ignore"
 
-    def __init__(self):
-        self._path = Path("")
+    def __init__(self) -> None:
+        self._path = Path()
         self._ignored_lines = []
 
     def check_comment(self, vulture: Item) -> bool:
         """Check if the vulture output line is ignored with a # vulture: ignore comment
-         Examples::
-            >>> Path("/tmp/test.py").write_text("def test():pass")
-            15
-            >>> finder = CommentFinder()
-            >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 1, 1, "unused function 'test'", 50))
-            False
-            >>> Path("/tmp/test.py").write_text('def test():  # vulture: ignore\\n     pass')
-            40
-            >>> finder = CommentFinder()  # the file has changed, must recreate the instance
-            >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 1, 1, "unused function 'test'", 50))
-            True
-            >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 3, 3, "unused function 'test'", 50))
-            False
+        Examples::
+           >>> Path("/tmp/test.py").write_text("def test():pass")
+           15
+           >>> finder = CommentFinder()
+           >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 1, 1, "unused function 'test'", 50))
+           False
+           >>> Path("/tmp/test.py").write_text('def test():  # vulture: ignore\\n     pass')
+           40
+           >>> finder = CommentFinder()  # the file has changed, must recreate the instance
+           >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 1, 1, "unused function 'test'", 50))
+           True
+           >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 3, 3, "unused function 'test'", 50))
+           False
         """
         line_number = vulture.first_lineno
         if line_number is None:  # pragma: no cover
@@ -44,48 +49,51 @@ def check_comment(self, vulture: Item) -> bool:
             self.__reset(vulture.filename)
         return self.__find_rec(vulture)
 
-    def __find_rec(self, vulture: Item, tree=None, ignore_mode=False) -> bool:
-        """Find comments recursively"""
+    def __find_rec(self, vulture: Item, tree: "Optional[AST]" = None, *, ignore_mode: bool = False) -> bool:
+        """Find comments recursively."""
         if tree is None:
             tree = self._tree
+        if tree is None:  # pragma: no cover
+            return False
         with suppress(AttributeError):
-            line_nb = tree.lineno
-            if not ignore_mode:
-                if line_nb in self._ignored_lines:
-                    ignore_mode = True
-            if ignore_mode:
-                if vulture.first_lineno in [line_nb, line_nb - self.__get_decorators(tree)]:
-                    return True
+            line_nb: int = getattr(tree, "lineno")  # noqa: B009
+            if not ignore_mode and line_nb in self._ignored_lines:
+                ignore_mode = True
+            if ignore_mode and vulture.first_lineno in [
+                line_nb,
+                line_nb - self.__get_decorators(tree),
+            ]:
+                return True
         with suppress(AttributeError):
-            for new_tree in tree.body:
+            for new_tree in tree.body:  # type: ignore[attr-defined]
                 if self.__find_rec(vulture, tree=new_tree, ignore_mode=ignore_mode):
                     return True
         with suppress(AttributeError):
-            for new_tree in tree.orelse:
+            for new_tree in tree.orelse:  # type: ignore[attr-defined]
                 if self.__find_rec(vulture, tree=new_tree, ignore_mode=ignore_mode):
                     return True
         return False
 
     @staticmethod
-    def __get_decorators(tree) -> int:
+    def __get_decorators(tree: "Optional[AST]") -> int:
+        if tree is None:  # pragma: no cover
+            return 0
         try:
-            return len(tree.decorator_list)
+            return len(tree.decorator_list)  # type: ignore[attr-defined]
         except AttributeError:
             return 0
 
-    def __reset(self, path: Path):
+    def __reset(self, path: Path) -> None:
         self._path = path
         self._ignored_lines = []
         try:
             self._content = self._path.read_text(encoding="utf-8-sig")
             self._tree = ast.parse(self._content)
             content_split = self._content.split("\n")
-            index_line = 0
-            for elem in content_split:
+            for index_line, elem in enumerate(content_split):
                 if self._VULTURE_IGNORE in elem:
                     self._ignored_lines.append(index_line + 1)
-                index_line += 1
 
         except OSError as err:  # pragma: no cover
-            print(f"warning : unable to read : {self._path.as_posix()} : {err}")
+            print(f"warning : unable to read : {self._path.as_posix()} : {err}")  # noqa: T201
             self._content = ""
diff --git a/src/pytest_vulture/vulture/manager.py b/src/pytest_vulture/vulture/manager.py
index 0df2455..1f67d22 100644
--- a/src/pytest_vulture/vulture/manager.py
+++ b/src/pytest_vulture/vulture/manager.py
@@ -1,6 +1,6 @@
-"""Manages vulture"""
-import re
+"""Manages vulture."""
 
+import re
 from pathlib import Path
 from typing import (
     List,
@@ -16,88 +16,86 @@
 
 
 class VultureManager:
-    """The vulture manager for pytest"""
+    """The vulture manager for pytest."""
+
+    __root_dir: Path
     _results: List[Item]
     _config: IniReader
     _setup_manager: SetupManager
     _COMMENT_FINDER: CommentFinder = CommentFinder()
 
-    def __init__(self, root_dir, config: IniReader):
+    def __init__(self, root_dir: Path, config: IniReader) -> None:
         """Type of root_dir can be py._path.local.LocalPath. It depends on the version of pytest."""
         self.__root_dir = root_dir
         self._config = config
         self._results = []
         self.__setup_manager = SetupManager(self._config)
 
-    def call(self):
+    def call(self) -> None:
         """Call vulture on a folder
         Example::
             >>> import py
-            >>> from py._path.local import LocalPath
-            >>> config = IniReader(path_ini=Path("vulture.ini"))
+            >>> config = IniReader(path_ini=Path("vulture.ini"), is_pyproject=False)
             >>> config.read_ini()
-            >>> manager = VultureManager(LocalPath("/tmp"), config)
+            >>> manager = VultureManager(Path("/tmp"), config)
             >>> manager.call()
         """
         vulture_configuration = self._config.vulture_configuration
         vulture = Vulture(
             ignore_names=vulture_configuration.ignore_names,
-            ignore_decorators=vulture_configuration.ignore_decorators
+            ignore_decorators=vulture_configuration.ignore_decorators,
         )
         vulture.scavenge([str(self.__root_dir)], exclude=vulture_configuration.exclude)
         self._results = [elem for elem in vulture.get_unused_code() if not self.__filter_results(elem)]
 
-    def get_file_errors(self, path) -> Optional[str]:
+    def get_file_errors(self, path: str) -> Optional[str]:
         """Get file errors
         Example::
             >>> import py
             >>> from pathlib import Path
-            >>> from py._path.local import LocalPath
             >>> Path("/tmp/package").mkdir(exist_ok=True)
             >>> Path("/tmp/package/test.py").write_text("def test():pass")
             15
             >>> config = IniReader(path_ini=Path("vulture.ini"))
             >>> config.read_ini()
-            >>> manager = VultureManager(LocalPath("/tmp/package"), config)
+            >>> manager = VultureManager(Path("/tmp/package"), config)
             >>> manager.call()
-            >>> manager.get_file_errors(LocalPath("/tmp/package/test.py"))
+            >>> manager.get_file_errors(Path("/tmp/package/test.py"))
             "line 1 : unused function 'test'"
         """
         errors = []
         for error_vulture in self._results:
             if self.__path_equals(path, error_vulture.filename.as_posix(), str(self.__root_dir)):
-                errors.append(f"line {error_vulture.first_lineno} : {error_vulture.message}")
+                errors.append(f"line {error_vulture.first_lineno} : {error_vulture.message}")  # noqa: PERF401
         if not errors:
             return None
         return "\n".join(errors)
 
     def __filter_results(self, vulture_output: Item) -> bool:
-        """Check if the vulture output concerns an ignore path, an entry point or a # vulture: ignore"""
+        """Check if the vulture output concerns an ignore path, an entry point or a # vulture: ignore."""
         for ignored_path in self._config.vulture_configuration.ignore:
             if self.__check_path(ignored_path, vulture_output.filename):
                 return True
-        return self.__setup_manager.is_entry_point(
-            vulture_output
-        ) or self.__check_ignore_type(
-            vulture_output
-        ) or self._COMMENT_FINDER.check_comment(
-            vulture_output
+        return (
+            self.__setup_manager.is_entry_point(vulture_output)
+            or self.__check_ignore_type(vulture_output)
+            or self._COMMENT_FINDER.check_comment(vulture_output)
         )
 
     def __check_path(self, path_exclude: str, path_to_check: Path) -> bool:
-        match = re.compile(path_exclude.replace("*", ".*")).match(path_to_check.as_posix())
+        match = re.compile(path_exclude.replace("*", ".*")).match(path_to_check.as_posix()) or re.compile(
+            str(self.__root_dir / path_exclude).replace("*", ".*")
+        ).match(path_to_check.as_posix())
         if not match:
-            return self.__path_equals(
-                path_exclude, path_to_check.absolute().as_posix(), str(self.__root_dir)
-            )
+            return self.__path_equals(path_exclude, path_to_check.absolute().as_posix(), str(self.__root_dir))
         return match.group() == path_to_check.as_posix()
 
-    def __check_ignore_type(self, vulture_output: Item):
+    def __check_ignore_type(self, vulture_output: Item) -> bool:
         return vulture_output.typ in self._config.vulture_configuration.ignore_types
 
     @classmethod
-    def __path_equals(cls, path_1, path_2, root_dir: str) -> bool:
-        """Manages tox and pytest weird path usages, to simplify"""
+    def __path_equals(cls, path_1: str, path_2: str, root_dir: str) -> bool:
+        """Manage tox and pytest weird path usages, to simplify."""
         path_1_full = (Path(root_dir) / Path(str(path_1))).as_posix()
         path_2_full = (Path(root_dir) / Path(str(path_2))).as_posix()
         return path_1_full == path_2_full
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..3cb2b42
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,4 @@
+pytest
+pytest-runner
+pytest-cov
+pytest-mypy
diff --git a/test/__init__.py b/test/__init__.py
index 109fa9c..4af7820 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,5 +1,6 @@
-"""The pytest plugin tests"""
+"""The pytest plugin tests."""
+
 import os
 
 
-TEST_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+TEST_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # noqa: PTH100,PTH120
diff --git a/test/examples/ini/pyproject.toml b/test/examples/ini/pyproject.toml
new file mode 100644
index 0000000..b6230de
--- /dev/null
+++ b/test/examples/ini/pyproject.toml
@@ -0,0 +1,38 @@
+[build-system]
+build-backend = "setuptools.build_meta"
+requires = [
+    "setuptools",
+]
+
+[project]
+name = "test"
+version = "3"
+requires-python = ">=3.9"
+dynamic = [
+    "dependencies",
+]
+[project.scripts]
+a-b = "c.d.e:f"
+g-h = "i.cli.j:k"
+
+
+[tool.setuptools.dynamic]
+dependencies = { file = ["requirements/main-no-hash.txt"] }
+
+[tool.vulture]
+exclude = [
+    "*/test/*",
+]
+ignore = [
+    "src/some_code/*",
+]
+ignore-names = [
+    "delimiter",
+]
+ignore-decorators = [
+    "@override",
+]
+ignore-types = [
+    "import"
+]
+source-path = "src"
diff --git a/test/tests/__init__.py b/test/tests/__init__.py
index 87485d0..3ef1063 100644
--- a/test/tests/__init__.py
+++ b/test/tests/__init__.py
@@ -1,1 +1,1 @@
-"""All the tests"""
+"""All the tests."""
diff --git a/test/tests/conftest.py b/test/tests/conftest.py
index 918c870..c6ae450 100644
--- a/test/tests/conftest.py
+++ b/test/tests/conftest.py
@@ -1,17 +1,16 @@
-"""The conftest"""
+"""The conftest."""
+
 from pathlib import Path
 from shutil import copytree
-from test import TEST_BASE_DIR
 
 import pytest
 
+from test import TEST_BASE_DIR
+
 
 @pytest.fixture
 def examples_path(tmp_path):
-    """Get copies of the example"""
+    """Get copies of the example."""
     mock_dir = Path(TEST_BASE_DIR + "/test/examples")
-    copytree(
-        mock_dir,
-        tmp_path / "examples"
-    )
+    copytree(mock_dir, tmp_path / "examples")
     return tmp_path / "examples"
diff --git a/test/tests/functional/__init__.py b/test/tests/functional/__init__.py
index 70ba74d..364c1e6 100644
--- a/test/tests/functional/__init__.py
+++ b/test/tests/functional/__init__.py
@@ -1,1 +1,1 @@
-"""the functional tests"""
+"""the functional tests."""
diff --git a/test/tests/functional/test_call.py b/test/tests/functional/test_call.py
index a5acd21..ac0210b 100644
--- a/test/tests/functional/test_call.py
+++ b/test/tests/functional/test_call.py
@@ -1,5 +1,5 @@
-"""tests the configuration system"""
-# pylint: disable=redefined-outer-name,protected-access,too-many-arguments
+"""tests the configuration system."""
+
 from pathlib import Path
 from unittest.mock import Mock
 
@@ -9,15 +9,15 @@
 
 
 @pytest.mark.parametrize(
-    "folder_path,results",
+    ("folder_path", "results"),
     [
         ("vulture_tests/comments", 0),
         ("vulture_tests/bug_18_2020", 0),
         ("vulture_tests/easy", 1),
-    ]
+    ],
 )
 def test_with_comments(examples_path, folder_path, results):
-    """Tests the call function with comments to see if the vulture ignore disables the outputs"""
+    """Tests the call function with comments to see if the vulture ignore disables the outputs."""
     ini = Mock()
     ini.package_configuration.setup_path = Path("not_found.py")
     ini.vulture_configuration.exclude = []
@@ -33,21 +33,30 @@ def test_with_comments(examples_path, folder_path, results):
 
 
 @pytest.mark.parametrize(
-    "exclude,ignore,ignore_names,ignore_decorators,ignore_types,results,answer",
+    ("exclude", "ignore", "ignore_names", "ignore_decorators", "ignore_types", "results", "answer"),
     [
         ([], [], [], [], [], 1, "unused property 'main'"),
         ([".py"], [], [], [], [], 0, []),
-        (["main.py"], [], [], [], [], 1,  "unused function 'other'"),
+        (["main.py"], [], [], [], [], 1, "unused function 'other'"),
         ([], ["main.py"], [], [], [], 0, []),
         ([], ["*.py"], [], [], [], 0, []),
         ([], ["easy/main.py"], [], [], [], 1, "unused property 'main'"),
         ([], [], ["main"], [], [], 0, []),
         ([], [], [], ["@property"], [], 0, []),
         ([], [], [], [], ["property"], 0, []),
-    ]
+    ],
 )
-def test_call(examples_path, exclude, ignore, ignore_names, ignore_decorators, ignore_types, results, answer):
-    """Tests the call option with vulture with the easy example"""
+def test_call(
+    examples_path,
+    exclude,
+    ignore,
+    ignore_names,
+    ignore_decorators,
+    ignore_types,
+    results,
+    answer,
+):
+    """Tests the call option with vulture with the easy example."""
     ini = Mock()
     ini.package_configuration.setup_path = Path("not_found.py")
     ini.vulture_configuration.exclude = exclude
diff --git a/test/tests/functional/test_conf.py b/test/tests/functional/test_conf.py
index ff809d1..7bc9a25 100644
--- a/test/tests/functional/test_conf.py
+++ b/test/tests/functional/test_conf.py
@@ -1,13 +1,13 @@
-"""tests the configuration system"""
-# pylint: disable=redefined-outer-name
+"""tests the configuration system."""
+
 from pathlib import Path
 
 from pytest_vulture.conf.reader import IniReader
 
 
 def test_read_full_ini(examples_path):
-    """Tests with the full.ini example"""
-    reader = IniReader(Path(examples_path / "ini/full.ini"))
+    """Tests with the full.ini example."""
+    reader = IniReader(Path(examples_path / "ini/full.ini"), is_pyproject=False)
 
     reader.read_ini()
     exclude = reader.vulture_configuration.exclude
@@ -25,8 +25,8 @@ def test_read_full_ini(examples_path):
 
 
 def test_read_light_ini(examples_path):
-    """Tests with the mini.ini example"""
-    reader = IniReader(Path(examples_path / "ini/mini.ini"))
+    """Tests with the mini.ini example."""
+    reader = IniReader(Path(examples_path / "ini/mini.ini"), is_pyproject=False)
 
     reader.read_ini()
 
@@ -37,3 +37,24 @@ def test_read_light_ini(examples_path):
     assert reader.vulture_configuration.ignore_types == []
     assert reader.package_configuration.setup_path.as_posix() == "setup.py"
     assert reader.package_configuration.check_entry_points is True
+
+
+def test_read_pyproject_toml(examples_path):
+    reader = IniReader(Path(examples_path / "ini/pyproject.toml"), is_pyproject=True)
+
+    reader.read_ini()
+
+    assert reader.vulture_configuration.exclude == [
+        "*/.tox/*",
+        "*/.git/*",
+        "*/.cache/*",
+        "*/.idea/*",
+        "*/.eggs/*",
+        "*/test/*",
+    ]
+    assert reader.vulture_configuration.ignore_names == ["delimiter"]
+    assert reader.vulture_configuration.ignore_decorators == ["@override"]
+    assert reader.vulture_configuration.ignore == ["src/some_code/*"]
+    assert reader.vulture_configuration.ignore_types == ["import"]
+    assert reader.package_configuration.check_entry_points is True
+    assert reader.package_configuration.source_path.as_posix() == "src"
diff --git a/test/tests/functional/test_setup.py b/test/tests/functional/test_setup.py
index 669439a..19a609a 100644
--- a/test/tests/functional/test_setup.py
+++ b/test/tests/functional/test_setup.py
@@ -1,10 +1,9 @@
-"""tests the setup parser system to find entry points"""
+"""tests the setup parser system to find entry points."""
+
 from pathlib import Path
-# pylint: disable=redefined-outer-name,use-implicit-booleaness-not-comparison,too-many-arguments
 from unittest.mock import Mock
 
 import pytest
-
 from vulture.core import Item
 
 from pytest_vulture.setup_manager import (
@@ -15,24 +14,53 @@
 
 
 @pytest.mark.parametrize(
-    "check_entry_points,setup,message,file,first_lineno,is_entry_point",
+    ("check_entry_points", "setup", "message", "file", "first_lineno", "is_entry_point"),
     [
-        (False, "setup_with_entry_point", "unused function 'main'", "src/test/main.py", 8, True),
-        (False, "setup_with_entry_point", "unused function 'test'", "src/test/main.py", 8, False),
-        (False, "setup_with_entry_point", "unused property 'main'", "src/swagger_parser/conf/base.py", 8, False),
+        (
+            False,
+            "setup_with_entry_point",
+            "unused function 'main'",
+            "src/test/main.py",
+            8,
+            True,
+        ),
+        (
+            False,
+            "setup_with_entry_point",
+            "unused function 'test'",
+            "src/test/main.py",
+            8,
+            False,
+        ),
+        (
+            False,
+            "setup_with_entry_point",
+            "unused property 'main'",
+            "src/swagger_parser/conf/base.py",
+            8,
+            False,
+        ),
         (True, "setup", "unused function 'main'", "test/main.py", 7, True),
         (True, "setup", "unused function 'main'", "src/ttest/main.py", 7, False),
         (False, "setup_broken", "unused function 'main'", "main.py", 7, False),
         (False, "setup_no_entry_point", "unused function 'main'", "main.py", 7, False),
         (False, "setup_not_found", "unused function 'main'", "main.py", 7, False),
-    ]
+    ],
 )
-def test_is_entry_point(check_entry_points, examples_path, setup, message, file, first_lineno, is_entry_point):
-    """tests the entry point checking system"""
+def test_is_entry_point(
+    check_entry_points,
+    examples_path,
+    setup,
+    message,
+    file,
+    first_lineno,
+    is_entry_point,
+):
+    """Tests the entry point checking system."""
     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("")
+    conf.package_configuration.source_path = Path()
     setup_entry_point = SetupManager(conf)
     vulture = Item("test", "function", Path(file), first_lineno, first_lineno, message, 50)
 
@@ -40,17 +68,17 @@ def test_is_entry_point(check_entry_points, examples_path, setup, message, file,
 
 
 @pytest.mark.parametrize(
-    "path,error",
+    ("path", "error"),
     [
         ("setup_file_not_found", EntryPointFileError),
         ("setup_entry_p_not_found", EntryPointFunctionError),
-    ]
+    ],
 )
 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.source_path = Path()
     conf.package_configuration.setup_path = not_found_path
 
     with pytest.raises(error):
diff --git a/test/tests/unit/__init__.py b/test/tests/unit/__init__.py
index 82d6f15..e0310a0 100644
--- a/test/tests/unit/__init__.py
+++ b/test/tests/unit/__init__.py
@@ -1,1 +1,1 @@
-"""Unit tests"""
+"""Unit tests."""
diff --git a/test/tests/unit/test_line_parsing.py b/test/tests/unit/test_line_parsing.py
index 0571aab..1e6e076 100644
--- a/test/tests/unit/test_line_parsing.py
+++ b/test/tests/unit/test_line_parsing.py
@@ -1,24 +1,45 @@
-"""tests the vulture line parsing"""
-#  pylint: disable=protected-access
+"""tests the vulture line parsing."""
+
 from pathlib import Path
 from unittest.mock import Mock
 
 import pytest
-
 from vulture.core import Item
 
 from pytest_vulture.setup_manager import SetupManager
 
 
 @pytest.mark.parametrize(
-    "vulture_message,path",
+    ("vulture_message", "path"),
     [
-        (Item("test", "function", Path("src/test.py"), 15, 15, "unused function 'main'", 60), "src.test:main"),
-        (Item("test", "function", Path("src/tutu.py"), 15, 15, "unused function 'tata'", 60), "src.tutu:tata"),
-    ]
+        (
+            Item(
+                "test",
+                "function",
+                Path("src/test.py"),
+                15,
+                15,
+                "unused function 'main'",
+                60,
+            ),
+            "src.test:main",
+        ),
+        (
+            Item(
+                "test",
+                "function",
+                Path("src/tutu.py"),
+                15,
+                15,
+                "unused function 'tata'",
+                60,
+            ),
+            "src.tutu:tata",
+        ),
+    ],
 )
 def test_python_path(vulture_message, path, tmp_path):
     mock = Mock()
-    mock.package_configuration.source_path = Path("")
+    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
index 907c5d6..3371ce1 100644
--- a/test/tests/unit/test_setup.py
+++ b/test/tests/unit/test_setup.py
@@ -1,5 +1,5 @@
 """Tests the setup parser."""
-# pylint: disable=protected-access
+
 from pathlib import Path
 from unittest.mock import Mock
 
@@ -28,6 +28,7 @@
 dependencies = {file = ["requirements/main.txt"]}
 """
 
+
 def test_py_project(tmp_path):
     setup = tmp_path / "pyproject.toml"
     test_tools = tmp_path / "test_tools/cli"
@@ -39,10 +40,19 @@ def test_py_project(tmp_path):
     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
+    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
+    )
diff --git a/test/tests/unit/test_vulture_manager.py b/test/tests/unit/test_vulture_manager.py
index cc260d9..98fa53a 100644
--- a/test/tests/unit/test_vulture_manager.py
+++ b/test/tests/unit/test_vulture_manager.py
@@ -1,35 +1,83 @@
-"""tests vulture_manager.py"""
+"""tests vulture_manager.py."""
+
 from pathlib import Path
 
 import pytest
-
 from vulture.core import Item
 
 from pytest_vulture.conf.reader import IniReader
 from pytest_vulture.vulture.manager import VultureManager
 
 
-# pylint: disable=protected-access,too-many-arguments
-
-
-
-
-
 @pytest.mark.parametrize(
-    "results,file,answer",
+    ("results", "file", "answer"),
     [
         ([], "test.py", None),
-        ([Item("test", "function", Path("src/test.py"), 8, 8, "unused function 'test'", 50)], "not_test.py", None),
-        ([Item("test", "function", Path("src/test.py"), 8, 8, "unused function 'test'", 50)], "test.py", None),
-        ([Item("test", "function", Path("src/test.py"), 8, 8, "unused function 'test'", 50)], "src/test.py",
-         "line 8 : unused function 'test'"),
-        ([Item("test", "function", Path("src/test.py"), 9, 9, "unused function 'toto'", 50)], "src/test.py",
-         "line 9 : unused function 'toto'"),
-    ]
+        (
+            [
+                Item(
+                    "test",
+                    "function",
+                    Path("src/test.py"),
+                    8,
+                    8,
+                    "unused function 'test'",
+                    50,
+                )
+            ],
+            "not_test.py",
+            None,
+        ),
+        (
+            [
+                Item(
+                    "test",
+                    "function",
+                    Path("src/test.py"),
+                    8,
+                    8,
+                    "unused function 'test'",
+                    50,
+                )
+            ],
+            "test.py",
+            None,
+        ),
+        (
+            [
+                Item(
+                    "test",
+                    "function",
+                    Path("src/test.py"),
+                    8,
+                    8,
+                    "unused function 'test'",
+                    50,
+                )
+            ],
+            "src/test.py",
+            "line 8 : unused function 'test'",
+        ),
+        (
+            [
+                Item(
+                    "test",
+                    "function",
+                    Path("src/test.py"),
+                    9,
+                    9,
+                    "unused function 'toto'",
+                    50,
+                )
+            ],
+            "src/test.py",
+            "line 9 : unused function 'toto'",
+        ),
+    ],
 )
 def test_get_file_errors(tmp_path, results, file, answer):
-    """Tests the getting file"""
-    manager = VultureManager(tmp_path / "test2", IniReader(tmp_path / "test"))
+    """Test the getting file."""
+    manager = VultureManager(tmp_path / "test2", IniReader(tmp_path / "test", is_pyproject=False))
     manager._results = results
 
     assert manager.get_file_errors(file) == answer