77
88import typing
99from importlib .metadata import EntryPoint , entry_points
10+ from itertools import chain
1011from string import Template
1112from textwrap import dedent
12- from typing import Any , Callable , Iterable , List , Optional , Protocol
13+ from typing import (
14+ Any ,
15+ Callable ,
16+ Generator ,
17+ Iterable ,
18+ List ,
19+ NamedTuple ,
20+ Optional ,
21+ Protocol ,
22+ Union ,
23+ )
1324
1425from .. import __version__
1526from ..types import Plugin , Schema
1627
17- ENTRYPOINT_GROUP = "validate_pyproject.tool_schema"
18-
1928
2029class PluginProtocol (Protocol ):
2130 @property
@@ -66,34 +75,63 @@ def __repr__(self) -> str:
6675 return f"{ self .__class__ .__name__ } ({ self .tool !r} , { self .id } )"
6776
6877
78+ class StoredPlugin :
79+ def __init__ (self , tool : str , schema : Schema ):
80+ self ._tool , _ , self ._fragment = tool .partition ("#" )
81+ self ._schema = schema
82+
83+ @property
84+ def id (self ) -> str :
85+ return self .schema .get ("id" , "MISSING ID" )
86+
87+ @property
88+ def tool (self ) -> str :
89+ return self ._tool
90+
91+ @property
92+ def schema (self ) -> Schema :
93+ return self ._schema
94+
95+ @property
96+ def fragment (self ) -> str :
97+ return self ._fragment
98+
99+ @property
100+ def help_text (self ) -> str :
101+ return self .schema .get ("description" , "" )
102+
103+ def __repr__ (self ) -> str :
104+ args = [repr (self .tool ), self .id ]
105+ if self .fragment :
106+ args .append (f"fragment={ self .fragment !r} " )
107+ return f"{ self .__class__ .__name__ } ({ ', ' .join (args )} , <schema: { self .id } >)"
108+
109+
69110if typing .TYPE_CHECKING :
70111 _ : PluginProtocol = typing .cast (PluginWrapper , None )
71112
72113
73- def iterate_entry_points (group : str = ENTRYPOINT_GROUP ) -> Iterable [EntryPoint ]:
74- """Produces a generator yielding an EntryPoint object for each plugin registered
114+ def iterate_entry_points (group : str ) -> Iterable [EntryPoint ]:
115+ """Produces an iterable yielding an EntryPoint object for each plugin registered
75116 via ``setuptools`` `entry point`_ mechanism.
76117
77118 This method can be used in conjunction with :obj:`load_from_entry_point` to filter
78- the plugins before actually loading them.
119+ the plugins before actually loading them. The entry points are not
120+ deduplicated.
79121 """
80122 entries = entry_points ()
81123 if hasattr (entries , "select" ): # pragma: no cover
82124 # The select method was introduced in importlib_metadata 3.9 (and Python 3.10)
83125 # and the previous dict interface was declared deprecated
84126 select = typing .cast (
85- Any ,
127+ Callable [..., Iterable [ EntryPoint ]] ,
86128 getattr (entries , "select" ), # noqa: B009
87129 ) # 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 = {
94- e .name : e for e in sorted (entries_ , key = lambda e : (e .name , e .value ))
95- }
96- return list (deduplicated .values ())
130+ return select (group = group )
131+ # pragma: no cover
132+ # TODO: Once Python 3.10 becomes the oldest version supported, this fallback and
133+ # conditional statement can be removed.
134+ return (plugin for plugin in entries .get (group , []))
97135
98136
99137def load_from_entry_point (entry_point : EntryPoint ) -> PluginWrapper :
@@ -105,23 +143,64 @@ def load_from_entry_point(entry_point: EntryPoint) -> PluginWrapper:
105143 raise ErrorLoadingPlugin (entry_point = entry_point ) from ex
106144
107145
146+ def load_from_multi_entry_point (
147+ entry_point : EntryPoint ,
148+ ) -> Generator [StoredPlugin , None , None ]:
149+ """Carefully load the plugin, raising a meaningful message in case of errors"""
150+ try :
151+ fn = entry_point .load ()
152+ output = fn ()
153+ except Exception as ex :
154+ raise ErrorLoadingPlugin (entry_point = entry_point ) from ex
155+
156+ for tool , schema in output ["tools" ].items ():
157+ yield StoredPlugin (tool , schema )
158+ for schema in output .get ("schemas" , []):
159+ yield StoredPlugin ("" , schema )
160+
161+
162+ class _SortablePlugin (NamedTuple ):
163+ priority : int
164+ name : str
165+ plugin : Union [PluginWrapper , StoredPlugin ]
166+
167+ def __lt__ (self , other : Any ) -> bool :
168+ return (self .plugin .tool or self .plugin .id , self .name , self .priority ) < (
169+ other .plugin .tool or other .plugin .id ,
170+ other .name ,
171+ other .priority ,
172+ )
173+
174+
108175def list_from_entry_points (
109- group : str = ENTRYPOINT_GROUP ,
110176 filtering : Callable [[EntryPoint ], bool ] = lambda _ : True ,
111- ) -> List [PluginWrapper ]:
177+ ) -> List [Union [ PluginWrapper , StoredPlugin ] ]:
112178 """Produces a list of plugin objects for each plugin registered
113179 via ``setuptools`` `entry point`_ mechanism.
114180
115181 Args:
116- group: name of the setuptools' entry point group where plugins is being
117- registered
118182 filtering: function returning a boolean deciding if the entry point should be
119183 loaded and included (or not) in the final list. A ``True`` return means the
120184 plugin should be included.
121185 """
122- return [
123- load_from_entry_point (e ) for e in iterate_entry_points (group ) if filtering (e )
124- ]
186+ tool_eps = (
187+ _SortablePlugin (0 , e .name , load_from_entry_point (e ))
188+ for e in iterate_entry_points ("validate_pyproject.tool_schema" )
189+ if filtering (e )
190+ )
191+ multi_eps = (
192+ _SortablePlugin (1 , e .name , p )
193+ for e in sorted (
194+ iterate_entry_points ("validate_pyproject.multi_schema" ),
195+ key = lambda e : e .name ,
196+ reverse = True ,
197+ )
198+ for p in load_from_multi_entry_point (e )
199+ if filtering (e )
200+ )
201+ eps = chain (tool_eps , multi_eps )
202+ dedup = {e .plugin .tool or e .plugin .id : e .plugin for e in sorted (eps , reverse = True )}
203+ return list (dedup .values ())[::- 1 ]
125204
126205
127206class ErrorLoadingPlugin (RuntimeError ):
0 commit comments