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

abravalheri / validate-pyproject / 11034501584

25 Sep 2024 01:49PM CUT coverage: 97.823% (-0.2%) from 97.976%
11034501584

push

github

abravalheri
Prevent Github action for ignoring files for cache

551 of 571 branches covered (96.5%)

Branch coverage included in aggregate %.

932 of 945 relevant lines covered (98.62%)

5.91 hits per line

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

93.81
/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
"""
6✔
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 string import Template
6✔
11
from textwrap import dedent
6✔
12
from typing import Any, Callable, Iterable, List, Optional, Protocol
6✔
13

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

17
ENTRYPOINT_GROUP = "validate_pyproject.tool_schema"
6✔
18

19

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

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

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

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

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

36

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

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

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

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

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

58
    @property
6✔
59
    def help_text(self) -> str:
6✔
60
        tpl = self._load_fn.__doc__
6✔
61
        if not tpl:
6✔
62
            return ""
6✔
63
        return Template(tpl).safe_substitute(tool=self.tool, id=self.id)
6✔
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]:
6✔
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()
6✔
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 = {
6✔
94
        e.name: e for e in sorted(entries_, key=lambda e: (e.name, e.value))
95
    }
96
    return list(deduplicated.values())
6✔
97

98

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

107

108
def list_from_entry_points(
6✔
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 [
6✔
123
        load_from_entry_point(e) for e in iterate_entry_points(group) if filtering(e)
124
    ]
125

126

127
class ErrorLoadingPlugin(RuntimeError):
6✔
128
    _DESC = """There was an error loading '{plugin}'.
6✔
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
6✔
133

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

138
        sub = {"package": __package__, "version": __version__, "plugin": plugin}
6!
139
        msg = dedent(self._DESC).format(**sub).splitlines()
6✔
140
        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

© 2025 Coveralls, Inc