• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

abravalheri / validate-pyproject / 6173991897923584

11 Nov 2024 04:41PM CUT coverage: 97.859%. Remained the same
6173991897923584

Pull #218

cirrus-ci

pre-commit-ci[bot]
[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
Pull Request #218: [pre-commit.ci] pre-commit autoupdate

293 of 306 branches covered (95.75%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

941 of 955 relevant lines covered (98.53%)

6.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.41
/src/validate_pyproject/plugins/__init__.py
1
# The code in this module is mostly borrowed/adapted from PyScaffold and was originally
2
# published under the MIT license
3
# The original PyScaffold license can be found in 'NOTICE.txt'
4
"""
7✔
5
.. _entry point: https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html
6
"""
7

8
import typing
7✔
9
from importlib.metadata import EntryPoint, entry_points
7✔
10
from string import Template
7✔
11
from textwrap import dedent
7✔
12
from typing import Any, Callable, Iterable, List, Optional, Protocol
7✔
13

14
from .. import __version__
7✔
15
from ..types import Plugin, Schema
7✔
16

17
ENTRYPOINT_GROUP = "validate_pyproject.tool_schema"
7✔
18

19

20
class PluginProtocol(Protocol):
7✔
21
    @property
7✔
22
    def id(self) -> str: ...
7!
23

24
    @property
7✔
25
    def tool(self) -> str: ...
7!
26

27
    @property
7✔
28
    def schema(self) -> Schema: ...
7!
29

30
    @property
7✔
31
    def help_text(self) -> str: ...
7!
32

33
    @property
7✔
34
    def fragment(self) -> str: ...
7!
35

36

37
class PluginWrapper:
7✔
38
    def __init__(self, tool: str, load_fn: Plugin):
7✔
39
        self._tool = tool
7✔
40
        self._load_fn = load_fn
7✔
41

42
    @property
7✔
43
    def id(self) -> str:
7✔
44
        return f"{self._load_fn.__module__}.{self._load_fn.__name__}"
7✔
45

46
    @property
7✔
47
    def tool(self) -> str:
7✔
48
        return self._tool
7✔
49

50
    @property
7✔
51
    def schema(self) -> Schema:
7✔
52
        return self._load_fn(self.tool)
7✔
53

54
    @property
7✔
55
    def fragment(self) -> str:
7✔
56
        return ""
7✔
57

58
    @property
7✔
59
    def help_text(self) -> str:
7✔
60
        tpl = self._load_fn.__doc__
7✔
61
        if not tpl:
7✔
62
            return ""
7✔
63
        return Template(tpl).safe_substitute(tool=self.tool, id=self.id)
7✔
64

65
    def __repr__(self) -> str:
66
        return f"{self.__class__.__name__}({self.tool!r}, {self.id})"
67

68

69
if typing.TYPE_CHECKING:
70
    _: PluginProtocol = typing.cast(PluginWrapper, None)
71

72

73
def iterate_entry_points(group: str = ENTRYPOINT_GROUP) -> Iterable[EntryPoint]:
7✔
74
    """Produces a generator yielding an EntryPoint object for each plugin registered
75
    via ``setuptools`` `entry point`_ mechanism.
76

77
    This method can be used in conjunction with :obj:`load_from_entry_point` to filter
78
    the plugins before actually loading them.
79
    """
80
    entries = entry_points()
7✔
81
    if hasattr(entries, "select"):  # pragma: no cover
82
        # The select method was introduced in importlib_metadata 3.9 (and Python 3.10)
83
        # and the previous dict interface was declared deprecated
84
        select = typing.cast(
85
            Any,
86
            getattr(entries, "select"),  # noqa: B009
87
        )  # typecheck gymnastics
88
        entries_: Iterable[EntryPoint] = select(group=group)
89
    else:  # pragma: no cover
90
        # TODO: Once Python 3.10 becomes the oldest version supported, this fallback and
91
        #       conditional statement can be removed.
92
        entries_ = (plugin for plugin in entries.get(group, []))
93
    deduplicated = {
7✔
94
        e.name: e for e in sorted(entries_, key=lambda e: (e.name, e.value))
95
    }
96
    return list(deduplicated.values())
7✔
97

98

99
def load_from_entry_point(entry_point: EntryPoint) -> PluginWrapper:
7✔
100
    """Carefully load the plugin, raising a meaningful message in case of errors"""
101
    try:
7✔
102
        fn = entry_point.load()
7✔
103
        return PluginWrapper(entry_point.name, fn)
7✔
104
    except Exception as ex:
7✔
105
        raise ErrorLoadingPlugin(entry_point=entry_point) from ex
7✔
106

107

108
def list_from_entry_points(
7✔
109
    group: str = ENTRYPOINT_GROUP,
110
    filtering: Callable[[EntryPoint], bool] = lambda _: True,
111
) -> List[PluginWrapper]:
112
    """Produces a list of plugin objects for each plugin registered
113
    via ``setuptools`` `entry point`_ mechanism.
114

115
    Args:
116
        group: name of the setuptools' entry point group where plugins is being
117
            registered
118
        filtering: function returning a boolean deciding if the entry point should be
119
            loaded and included (or not) in the final list. A ``True`` return means the
120
            plugin should be included.
121
    """
122
    return [
7✔
123
        load_from_entry_point(e) for e in iterate_entry_points(group) if filtering(e)
124
    ]
125

126

127
class ErrorLoadingPlugin(RuntimeError):
7✔
128
    _DESC = """There was an error loading '{plugin}'.
7✔
129
    Please make sure you have installed a version of the plugin that is compatible
130
    with {package} {version}. You can also try uninstalling it.
131
    """
132
    __doc__ = _DESC
7✔
133

134
    def __init__(self, plugin: str = "", entry_point: Optional[EntryPoint] = None):
7✔
135
        if entry_point and not plugin:
7!
136
            plugin = getattr(entry_point, "module", entry_point.name)
7✔
137

138
        sub = {"package": __package__, "version": __version__, "plugin": plugin}
7✔
139
        msg = dedent(self._DESC).format(**sub).splitlines()
7✔
140
        super().__init__(f"{msg[0]}\n{' '.join(msg[1:])}")
7✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc