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

abravalheri / validate-pyproject / 13878387125

13 Mar 2025 10:29PM UTC coverage: 97.991%. First build
13878387125

push

github

web-flow
fix: more readable plugin id for stored plugins (#241)

80 of 88 branches covered (90.91%)

Branch coverage included in aggregate %.

13 of 14 new or added lines in 2 files covered. (92.86%)

993 of 1007 relevant lines covered (98.61%)

5.9 hits per line

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

94.78
/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
"""
5
.. _entry point: https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html
6
"""
7

8
import typing
6✔
9
from importlib.metadata import EntryPoint, entry_points
6✔
10
from itertools import chain
6✔
11
from string import Template
6✔
12
from textwrap import dedent
6✔
13
from typing import (
6✔
14
    Any,
15
    Callable,
16
    Generator,
17
    Iterable,
18
    List,
19
    NamedTuple,
20
    Optional,
21
    Protocol,
22
    Union,
23
)
24

25
from .. import __version__
6✔
26
from ..types import Plugin, Schema
6✔
27

28

29
class PluginProtocol(Protocol):
6✔
30
    @property
6✔
31
    def id(self) -> str: ...
6!
32

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

36
    @property
6✔
37
    def schema(self) -> Schema: ...
6!
38

39
    @property
6✔
40
    def help_text(self) -> str: ...
6!
41

42
    @property
6✔
43
    def fragment(self) -> str: ...
6✔
44

45

46
class PluginWrapper:
6✔
47
    def __init__(self, tool: str, load_fn: Plugin):
6✔
48
        self._tool = tool
6✔
49
        self._load_fn = load_fn
6✔
50

51
    @property
6✔
52
    def id(self) -> str:
6✔
53
        return f"{self._load_fn.__module__}.{self._load_fn.__name__}"
6✔
54

55
    @property
6✔
56
    def tool(self) -> str:
6✔
57
        return self._tool
6✔
58

59
    @property
6✔
60
    def schema(self) -> Schema:
6✔
61
        return self._load_fn(self.tool)
6✔
62

63
    @property
6✔
64
    def fragment(self) -> str:
6✔
65
        return ""
6✔
66

67
    @property
6✔
68
    def help_text(self) -> str:
6✔
69
        tpl = self._load_fn.__doc__
6✔
70
        if not tpl:
6✔
71
            return ""
6✔
72
        return Template(tpl).safe_substitute(tool=self.tool, id=self.id)
6✔
73

74
    def __repr__(self) -> str:
75
        return f"{self.__class__.__name__}({self.tool!r}, {self.id})"
76

77
    def __str__(self) -> str:
6✔
78
        return self.id
6✔
79

80

81
class StoredPlugin:
6✔
82
    def __init__(self, tool: str, schema: Schema, source: str):
6✔
83
        self._tool, _, self._fragment = tool.partition("#")
6✔
84
        self._schema = schema
6✔
85
        self._source = source
6✔
86

87
    @property
6✔
88
    def id(self) -> str:
6✔
89
        return self._schema["$id"]  # type: ignore[no-any-return]
6✔
90

91
    @property
6✔
92
    def tool(self) -> str:
6✔
93
        return self._tool
6✔
94

95
    @property
6✔
96
    def schema(self) -> Schema:
6✔
97
        return self._schema
6✔
98

99
    @property
6✔
100
    def fragment(self) -> str:
6✔
101
        return self._fragment
6✔
102

103
    @property
6✔
104
    def help_text(self) -> str:
6✔
105
        return self.schema.get("description", "")
6✔
106

107
    def __str__(self) -> str:
6✔
NEW
108
        return self._source
×
109

110
    def __repr__(self) -> str:
111
        args = [repr(self.tool), self.id]
112
        if self.fragment:
113
            args.append(f"fragment={self.fragment!r}")
114
        return f"{self.__class__.__name__}({', '.join(args)}, <schema: {self.id}>)"
115

116

117
if typing.TYPE_CHECKING:
118
    _: PluginProtocol = typing.cast(PluginWrapper, None)
119

120

121
def iterate_entry_points(group: str) -> Iterable[EntryPoint]:
6✔
122
    """Produces an iterable yielding an EntryPoint object for each plugin registered
123
    via ``setuptools`` `entry point`_ mechanism.
124

125
    This method can be used in conjunction with :obj:`load_from_entry_point` to filter
126
    the plugins before actually loading them. The entry points are not
127
    deduplicated.
128
    """
129
    entries = entry_points()
6✔
130
    if hasattr(entries, "select"):  # pragma: no cover
131
        # The select method was introduced in importlib_metadata 3.9 (and Python 3.10)
132
        # and the previous dict interface was declared deprecated
133
        select = typing.cast(
134
            Callable[..., Iterable[EntryPoint]],
135
            getattr(entries, "select"),  # noqa: B009
136
        )  # typecheck gymnastics
137
        return select(group=group)
138
    # pragma: no cover
139
    # TODO: Once Python 3.10 becomes the oldest version supported, this fallback and
140
    #       conditional statement can be removed.
141
    return (plugin for plugin in entries.get(group, []))
3✔
142

143

144
def load_from_entry_point(entry_point: EntryPoint) -> PluginWrapper:
6✔
145
    """Carefully load the plugin, raising a meaningful message in case of errors"""
146
    try:
6✔
147
        fn = entry_point.load()
6✔
148
        return PluginWrapper(entry_point.name, fn)
6✔
149
    except Exception as ex:
6✔
150
        raise ErrorLoadingPlugin(entry_point=entry_point) from ex
6✔
151

152

153
def load_from_multi_entry_point(
6✔
154
    entry_point: EntryPoint,
155
) -> Generator[StoredPlugin, None, None]:
156
    """Carefully load the plugin, raising a meaningful message in case of errors"""
157
    try:
6✔
158
        fn = entry_point.load()
6✔
159
        output = fn()
6✔
160
        id_ = f"{fn.__module__}.{fn.__name__}"
6✔
161
    except Exception as ex:
6✔
162
        raise ErrorLoadingPlugin(entry_point=entry_point) from ex
6✔
163

164
    for tool, schema in output["tools"].items():
6✔
165
        yield StoredPlugin(tool, schema, f"{id_}:{tool}")
6✔
166
    for i, schema in enumerate(output.get("schemas", [])):
6✔
167
        yield StoredPlugin("", schema, f"{id_}:{i}")
6✔
168

169

170
class _SortablePlugin(NamedTuple):
6✔
171
    priority: int
6✔
172
    name: str
6✔
173
    plugin: Union[PluginWrapper, StoredPlugin]
6✔
174

175
    def __lt__(self, other: Any) -> bool:
6✔
176
        return (self.plugin.tool or self.plugin.id, self.name, self.priority) < (
6✔
177
            other.plugin.tool or other.plugin.id,
178
            other.name,
179
            other.priority,
180
        )
181

182

183
def list_from_entry_points(
6✔
184
    filtering: Callable[[EntryPoint], bool] = lambda _: True,
185
) -> List[Union[PluginWrapper, StoredPlugin]]:
186
    """Produces a list of plugin objects for each plugin registered
187
    via ``setuptools`` `entry point`_ mechanism.
188

189
    Args:
190
        filtering: function returning a boolean deciding if the entry point should be
191
            loaded and included (or not) in the final list. A ``True`` return means the
192
            plugin should be included.
193
    """
194
    tool_eps = (
6✔
195
        _SortablePlugin(0, e.name, load_from_entry_point(e))
196
        for e in iterate_entry_points("validate_pyproject.tool_schema")
197
        if filtering(e)
198
    )
199
    multi_eps = (
6✔
200
        _SortablePlugin(1, e.name, p)
201
        for e in sorted(
202
            iterate_entry_points("validate_pyproject.multi_schema"),
203
            key=lambda e: e.name,
204
            reverse=True,
205
        )
206
        for p in load_from_multi_entry_point(e)
207
        if filtering(e)
208
    )
209
    eps = chain(tool_eps, multi_eps)
6✔
210
    dedup = {e.plugin.tool or e.plugin.id: e.plugin for e in sorted(eps, reverse=True)}
6✔
211
    return list(dedup.values())[::-1]
6✔
212

213

214
class ErrorLoadingPlugin(RuntimeError):
6✔
215
    _DESC = """There was an error loading '{plugin}'.
6✔
216
    Please make sure you have installed a version of the plugin that is compatible
217
    with {package} {version}. You can also try uninstalling it.
218
    """
219
    __doc__ = _DESC
6✔
220

221
    def __init__(self, plugin: str = "", entry_point: Optional[EntryPoint] = None):
6✔
222
        if entry_point and not plugin:
6!
223
            plugin = getattr(entry_point, "module", entry_point.name)
6✔
224

225
        sub = {"package": __package__, "version": __version__, "plugin": plugin}
6✔
226
        msg = dedent(self._DESC).format(**sub).splitlines()
6✔
227
        super().__init__(f"{msg[0]}\n{' '.join(msg[1:])}")
6✔
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

© 2026 Coveralls, Inc