Skip to content

Commit cdb1a0b

Browse files
committed
Change default plugin priority but allow customisation
1 parent 40ebd81 commit cdb1a0b

File tree

3 files changed

+97
-29
lines changed

3 files changed

+97
-29
lines changed

docs/dev-guide.rst

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,11 @@ When using a :pep:`621`-compliant backend, the following can be add to your
118118
The plugin function will be automatically called with the ``tool_name``
119119
argument as same name as given to the entrypoint (e.g. :samp:`your_plugin({"your-tool"})`).
120120

121-
Also notice plugins are activated in a specific order, using Python's built-in
122-
``sorted`` function.
123-
124121

125122
Providing multiple schemas
126123
--------------------------
127124

128-
A second system is provided for providing multiple schemas in a single plugin.
125+
A second system is defined for providing multiple schemas in a single plugin.
129126
This is useful when a single plugin is responsible for multiple subtables
130127
under the ``tool`` table, or if you need to provide multiple schemas for a
131128
a single subtable.
@@ -158,6 +155,27 @@ An example of the plugin structure needed for this system is shown below:
158155
Fragments for schemas are also supported with this system; use ``#`` to split
159156
the tool name and fragment path in the dictionary key.
160157

158+
159+
.. admonition:: Experimental: Conflict Resolution
160+
161+
Please notice that when two plugins define the same ``tool``
162+
(or auxiliary schemas with the same ``$id``),
163+
an internal conflict resolution heuristic is employed to decide
164+
which schema will take effect.
165+
166+
To influence this heuristic you can:
167+
168+
- Define a numeric ``.priority`` property in the functions
169+
pointed by the ``validate_pyproject.tool_schema`` entry-points.
170+
- Add a ``"priority"`` key with a numeric value into the dictionary
171+
returned by the ``validate_pyproject.multi_schema`` plugins.
172+
173+
Typical values for ``priority`` are ``0`` and ``1``.
174+
175+
The exact order in which the plugins are loaded is considered an
176+
implementation detail.
177+
178+
161179
.. _entry-point: https://setuptools.pypa.io/en/stable/userguide/entry_point.html#entry-points
162180
.. _JSON Schema: https://json-schema.org/
163181
.. _Python package: https://packaging.python.org/

src/validate_pyproject/plugins/__init__.py

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
from .. import __version__
2626
from ..types import Plugin, Schema
2727

28+
_DEFAULT_MULTI_PRIORITY = 0
29+
_DEFAULT_TOOL_PRIORITY = 1
30+
2831

2932
class PluginProtocol(Protocol):
3033
@property
@@ -42,6 +45,9 @@ def help_text(self) -> str: ...
4245
@property
4346
def fragment(self) -> str: ...
4447

48+
@property
49+
def priority(self) -> float: ...
50+
4551

4652
class PluginWrapper:
4753
def __init__(self, tool: str, load_fn: Plugin):
@@ -64,6 +70,10 @@ def schema(self) -> Schema:
6470
def fragment(self) -> str:
6571
return ""
6672

73+
@property
74+
def priority(self) -> float:
75+
return getattr(self._load_fn, "priority", _DEFAULT_TOOL_PRIORITY)
76+
6777
@property
6878
def help_text(self) -> str:
6979
tpl = self._load_fn.__doc__
@@ -79,10 +89,11 @@ def __str__(self) -> str:
7989

8090

8191
class StoredPlugin:
82-
def __init__(self, tool: str, schema: Schema, source: str):
92+
def __init__(self, tool: str, schema: Schema, source: str, priority: float):
8393
self._tool, _, self._fragment = tool.partition("#")
8494
self._schema = schema
8595
self._source = source
96+
self._priority = priority
8697

8798
@property
8899
def id(self) -> str:
@@ -100,6 +111,10 @@ def schema(self) -> Schema:
100111
def fragment(self) -> str:
101112
return self._fragment
102113

114+
@property
115+
def priority(self) -> float:
116+
return self._priority
117+
103118
@property
104119
def help_text(self) -> str:
105120
return self.schema.get("description", "")
@@ -161,23 +176,34 @@ def load_from_multi_entry_point(
161176
except Exception as ex:
162177
raise ErrorLoadingPlugin(entry_point=entry_point) from ex
163178

179+
priority = output.get("priority", _DEFAULT_MULTI_PRIORITY)
164180
for tool, schema in output["tools"].items():
165-
yield StoredPlugin(tool, schema, f"{id_}:{tool}")
181+
yield StoredPlugin(tool, schema, f"{id_}:{tool}", priority)
166182
for i, schema in enumerate(output.get("schemas", [])):
167-
yield StoredPlugin("", schema, f"{id_}:{i}")
183+
yield StoredPlugin("", schema, f"{id_}:{i}", priority)
168184

169185

170186
class _SortablePlugin(NamedTuple):
171-
priority: int
172187
name: str
173188
plugin: Union[PluginWrapper, StoredPlugin]
174189

175190
def key(self) -> str:
176191
return self.plugin.tool or self.plugin.id
177192

178193
def __lt__(self, other: Any) -> bool:
179-
return (self.priority, self.name, self.key()) < (
180-
other.priority,
194+
# **Major concern**:
195+
# Consistency and reproducibility on which entry-points have priority
196+
# for a given environment.
197+
# The plugin with higher priority overwrites the schema definition.
198+
# The exact order that they are listed itself is not important for now.
199+
# **Implementation detail**:
200+
# By default, "single tool plugins" have priority 1 and "multi plugins"
201+
# have priority 0.
202+
# The order that the plugins will be listed is inverse to the priority.
203+
# If 2 plugins have the same numerical priority, the one whose
204+
# entry-point name is "higher alphabetically" wins.
205+
return (self.plugin.priority, self.name, self.key()) < (
206+
other.plugin.priority,
181207
other.name,
182208
other.key(),
183209
)
@@ -194,22 +220,13 @@ def list_from_entry_points(
194220
loaded and included (or not) in the final list. A ``True`` return means the
195221
plugin should be included.
196222
"""
197-
# **Major concern**:
198-
# Consistency and reproducibility on which entry-points have priority
199-
# for a given environment.
200-
# The plugin with higher priority overwrites the schema definition.
201-
# The exact order itself is not important for now.
202-
# **Implementation detail**:
203-
# Tool plugins are loaded first, so they are listed first than other schemas,
204-
# but multi plugins always have priority, overwriting the tool schemas.
205-
# The "higher alphabetically" an entry-point name, the more priority.
206223
tool_eps = (
207-
_SortablePlugin(0, e.name, load_from_entry_point(e))
224+
_SortablePlugin(e.name, load_from_entry_point(e))
208225
for e in iterate_entry_points("validate_pyproject.tool_schema")
209226
if filtering(e)
210227
)
211228
multi_eps = (
212-
_SortablePlugin(1, e.name, p)
229+
_SortablePlugin(e.name, p)
213230
for e in iterate_entry_points("validate_pyproject.multi_schema")
214231
for p in load_from_multi_entry_point(e)
215232
if filtering(e)

tests/test_plugins.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,14 @@ def test_empty_help_text(self):
8383
def _fn1(_):
8484
return {}
8585

86-
pw = plugins.StoredPlugin("name", {}, "id1")
86+
pw = plugins.StoredPlugin("name", {}, "id1", 0)
8787
assert pw.help_text == ""
8888

8989
def _fn2(_):
9090
"""Help for `${tool}`"""
9191
return {}
9292

93-
pw = plugins.StoredPlugin("name", {"description": "Help for me"}, "id2")
93+
pw = plugins.StoredPlugin("name", {"description": "Help for me"}, "id2", 0)
9494
assert pw.help_text == "Help for me"
9595

9696

@@ -158,30 +158,31 @@ def test_combined_plugins(monkeypatch, epname):
158158
"tools": {
159159
"example1": {"$id": "example1"},
160160
"example2": {"$id": "example2"},
161-
"example4": {"$id": "example4"},
161+
"example3": {"$id": "example3"},
162162
}
163163
}
164164
)
165165
tool_eps(name="example1", value="test_module:f1")(lambda _: {"$id": "ztool1"})
166166
tool_eps(name="example2", value="test_module:f2")(lambda _: {"$id": "atool2"})
167-
tool_eps(name="example3", value="test_module:f2")(lambda _: {"$id": "tool3"})
167+
tool_eps(name="example4", value="test_module:f2")(lambda _: {"$id": "tool4"})
168168

169169
monkeypatch.setattr(plugins, "iterate_entry_points", fake_eps.get)
170170

171171
lst = plugins.list_from_entry_points()
172+
print(lst)
172173
assert len(lst) == 4
173174

174175
assert lst[0].tool == "example1"
175-
assert isinstance(lst[0], StoredPlugin)
176+
assert isinstance(lst[0], PluginWrapper)
176177

177178
assert lst[1].tool == "example2"
178-
assert isinstance(lst[1], StoredPlugin)
179+
assert isinstance(lst[1], PluginWrapper)
179180

180181
assert lst[2].tool == "example3"
181-
assert isinstance(lst[2], PluginWrapper)
182+
assert isinstance(lst[2], StoredPlugin)
182183

183184
assert lst[3].tool == "example4"
184-
assert isinstance(lst[3], StoredPlugin)
185+
assert isinstance(lst[3], PluginWrapper)
185186

186187

187188
def test_several_multi_plugins(monkeypatch):
@@ -200,10 +201,42 @@ def test_several_multi_plugins(monkeypatch):
200201
monkeypatch.setattr(plugins, "iterate_entry_points", eps.get)
201202
# entry-point names closer to "zzzzzzzz..." have priority
202203
(plugin1, plugin2) = plugins.list_from_entry_points()
204+
print(plugin1, plugin2)
203205
assert plugin1.schema["$id"] == "example1"
204206
assert plugin2.schema["$id"] == "example3"
205207

206208

209+
def test_custom_priority(monkeypatch):
210+
fake_eps = _FakeEntryPoints(monkeypatch)
211+
tool_eps = fake_eps.group("validate_pyproject.tool_schema")
212+
multi_eps = fake_eps.group("validate_pyproject.multi_schema")
213+
214+
multi_schema = {"tools": {"example": {"$id": "multi-eps"}}}
215+
multi_eps(name="example", value="test_module:f")(lambda: multi_schema)
216+
217+
@tool_eps(name="example", value="test_module1:f1")
218+
def tool_schema1(_name):
219+
return {"$id": "tool-eps-1"}
220+
221+
@tool_eps(name="example", value="test_module2:f1")
222+
def tool_schema2(_name):
223+
return {"$id": "tool-eps-2"}
224+
225+
monkeypatch.setattr(plugins, "iterate_entry_points", fake_eps.get)
226+
(winner,) = plugins.list_from_entry_points() # default: tool with "higher" ep name
227+
assert winner.schema["$id"] == "tool-eps-2"
228+
229+
tool_schema1.priority = 1.1
230+
(winner,) = plugins.list_from_entry_points() # default: tool has priority
231+
assert winner.schema["$id"] == "tool-eps-1"
232+
233+
tool_schema1.priority = 0.1
234+
tool_schema2.priority = 0.2
235+
multi_schema["priority"] = 0.9
236+
(winner,) = plugins.list_from_entry_points() # custom higher priority wins
237+
assert winner.schema["$id"] == "multi-eps"
238+
239+
207240
def test_broken_multi_plugin(monkeypatch):
208241
fake_eps = _FakeEntryPoints(monkeypatch, "validate_pyproject.multi_schema")
209242
fake_eps(name="broken", value="test_module.f")(lambda: {}["no-such-key"])

0 commit comments

Comments
 (0)