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

abravalheri / validate-pyproject / 4665707205492736

13 Mar 2025 10:29PM UTC coverage: 98.052%. First build
4665707205492736

push

cirrus-ci

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

315 of 327 branches covered (96.33%)

Branch coverage included in aggregate %.

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

994 of 1008 relevant lines covered (98.61%)

8.86 hits per line

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

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

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

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

28

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

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

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

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

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

45

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

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

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

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

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

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

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

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

80

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

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

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

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

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

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

107
    def __str__(self) -> str:
9✔
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]:
9✔
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()
9✔
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, []))
1✔
142

143

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

152

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

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

169

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

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

182

183
def list_from_entry_points(
9✔
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 = (
9✔
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 = (
9✔
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)
9✔
210
    dedup = {e.plugin.tool or e.plugin.id: e.plugin for e in sorted(eps, reverse=True)}
9✔
211
    return list(dedup.values())[::-1]
9✔
212

213

214
class ErrorLoadingPlugin(RuntimeError):
9✔
215
    _DESC = """There was an error loading '{plugin}'.
9✔
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
9✔
220

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

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