Skip to content

Commit 63b7e90

Browse files
committed
hooks: keep hookimpls in a single list
The hookexec receives them in a single list (which is good), so make the HookCaller keep them in a single array as well, so can avoid the list concatenation in the hot call path. This makes adding a hookimpl a little slower, but not by much, and it is much colder than calling so the tradeoff makes sense. It is possible to optimize by keeping track of the splitpoint instead of calculating it every time, but I don't think it's worth it.
1 parent 2b9998a commit 63b7e90

File tree

3 files changed

+157
-44
lines changed

3 files changed

+157
-44
lines changed

src/pluggy/_hooks.py

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,7 @@ class _HookCaller:
286286
"name",
287287
"spec",
288288
"_hookexec",
289-
"_wrappers",
290-
"_nonwrappers",
289+
"_hookimpls",
291290
"_call_history",
292291
)
293292

@@ -300,8 +299,7 @@ def __init__(
300299
) -> None:
301300
self.name: "Final" = name
302301
self._hookexec: "Final" = hook_execute
303-
self._wrappers: "Final[List[HookImpl]]" = []
304-
self._nonwrappers: "Final[List[HookImpl]]" = []
302+
self._hookimpls: "Final[List[HookImpl]]" = []
305303
self._call_history: Optional[_CallHistory] = None
306304
self.spec: Optional[HookSpec] = None
307305
if specmodule_or_class is not None:
@@ -325,38 +323,38 @@ def is_historic(self) -> bool:
325323
return self._call_history is not None
326324

327325
def _remove_plugin(self, plugin: _Plugin) -> None:
328-
def remove(wrappers: List[HookImpl]) -> Optional[bool]:
329-
for i, method in enumerate(wrappers):
330-
if method.plugin == plugin:
331-
del wrappers[i]
332-
return True
333-
return None
334-
335-
if remove(self._wrappers) is None:
336-
if remove(self._nonwrappers) is None:
337-
raise ValueError(f"plugin {plugin!r} not found")
326+
for i, method in enumerate(self._hookimpls):
327+
if method.plugin == plugin:
328+
del self._hookimpls[i]
329+
return
330+
raise ValueError(f"plugin {plugin!r} not found")
338331

339332
def get_hookimpls(self) -> List["HookImpl"]:
340-
# Order is important for _hookexec
341-
return self._nonwrappers + self._wrappers
333+
return self._hookimpls.copy()
342334

343335
def _add_hookimpl(self, hookimpl: "HookImpl") -> None:
344336
"""Add an implementation to the callback chain."""
337+
for i, method in enumerate(self._hookimpls):
338+
if method.hookwrapper:
339+
splitpoint = i
340+
break
341+
else:
342+
splitpoint = len(self._hookimpls)
345343
if hookimpl.hookwrapper:
346-
methods = self._wrappers
344+
start, end = splitpoint, len(self._hookimpls)
347345
else:
348-
methods = self._nonwrappers
346+
start, end = 0, splitpoint
349347

350348
if hookimpl.trylast:
351-
methods.insert(0, hookimpl)
349+
self._hookimpls.insert(start, hookimpl)
352350
elif hookimpl.tryfirst:
353-
methods.append(hookimpl)
351+
self._hookimpls.insert(end, hookimpl)
354352
else:
355353
# find last non-tryfirst method
356-
i = len(methods) - 1
357-
while i >= 0 and methods[i].tryfirst:
354+
i = end - 1
355+
while i >= start and self._hookimpls[i].tryfirst:
358356
i -= 1
359-
methods.insert(i + 1, hookimpl)
357+
self._hookimpls.insert(i + 1, hookimpl)
360358

361359
def __repr__(self) -> str:
362360
return f"<_HookCaller {self.name!r}>"
@@ -387,7 +385,7 @@ def __call__(self, *args: object, **kwargs: object) -> Any:
387385
), "Cannot directly call a historic hook - use call_historic instead."
388386
self._verify_all_args_are_provided(kwargs)
389387
firstresult = self.spec.opts.get("firstresult", False) if self.spec else False
390-
return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
388+
return self._hookexec(self.name, self._hookimpls, kwargs, firstresult)
391389

392390
def call_historic(
393391
self,
@@ -406,7 +404,7 @@ def call_historic(
406404
self._call_history.append((kwargs, result_callback))
407405
# Historizing hooks don't return results.
408406
# Remember firstresult isn't compatible with historic.
409-
res = self._hookexec(self.name, self.get_hookimpls(), kwargs, False)
407+
res = self._hookexec(self.name, self._hookimpls, kwargs, False)
410408
if result_callback is None:
411409
return
412410
if isinstance(res, list):
@@ -429,13 +427,12 @@ def call_extra(
429427
"tryfirst": False,
430428
"specname": None,
431429
}
432-
hookimpls = self.get_hookimpls()
430+
hookimpls = self._hookimpls.copy()
433431
for method in methods:
434432
hookimpl = HookImpl(None, "<temp>", method, opts)
435433
# Find last non-tryfirst nonwrapper method.
436434
i = len(hookimpls) - 1
437-
until = len(self._nonwrappers)
438-
while i >= until and hookimpls[i].tryfirst:
435+
while i >= 0 and hookimpls[i].tryfirst and not hookimpls[i].hookwrapper:
439436
i -= 1
440437
hookimpls.insert(i + 1, hookimpl)
441438
firstresult = self.spec.opts.get("firstresult", False) if self.spec else False

testing/test_details.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,18 @@ def x1meth2(self):
3434
pm = MyPluginManager(hookspec.project_name)
3535
pm.register(Plugin())
3636
pm.add_hookspecs(Spec)
37-
assert not pm.hook.x1meth._nonwrappers[0].hookwrapper
38-
assert not pm.hook.x1meth._nonwrappers[0].tryfirst
39-
assert not pm.hook.x1meth._nonwrappers[0].trylast
40-
assert not pm.hook.x1meth._nonwrappers[0].optionalhook
4137

42-
assert pm.hook.x1meth2._wrappers[0].tryfirst
43-
assert pm.hook.x1meth2._wrappers[0].hookwrapper
38+
hookimpls = pm.hook.x1meth.get_hookimpls()
39+
assert len(hookimpls) == 1
40+
assert not hookimpls[0].hookwrapper
41+
assert not hookimpls[0].tryfirst
42+
assert not hookimpls[0].trylast
43+
assert not hookimpls[0].optionalhook
44+
45+
hookimpls = pm.hook.x1meth2.get_hookimpls()
46+
assert len(hookimpls) == 1
47+
assert hookimpls[0].hookwrapper
48+
assert hookimpls[0].tryfirst
4449

4550

4651
def test_warn_when_deprecated_specified(recwarn) -> None:
@@ -127,6 +132,6 @@ def myhook(self):
127132

128133
plugin = Plugin()
129134
pname = pm.register(plugin)
130-
assert repr(pm.hook.myhook._nonwrappers[0]) == (
135+
assert repr(pm.hook.myhook.get_hookimpls()[0]) == (
131136
f"<HookImpl plugin_name={pname!r}, plugin={plugin!r}>"
132137
)

testing/test_hookcaller.py

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def he_method2() -> None:
6161
def he_method3() -> None:
6262
pass
6363

64-
assert funcs(hc._nonwrappers) == [he_method1, he_method2, he_method3]
64+
assert funcs(hc.get_hookimpls()) == [he_method1, he_method2, he_method3]
6565

6666

6767
def test_adding_nonwrappers_trylast(hc: _HookCaller, addmeth: AddMeth) -> None:
@@ -77,7 +77,7 @@ def he_method1() -> None:
7777
def he_method1_b() -> None:
7878
pass
7979

80-
assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b]
80+
assert funcs(hc.get_hookimpls()) == [he_method1, he_method1_middle, he_method1_b]
8181

8282

8383
def test_adding_nonwrappers_trylast3(hc: _HookCaller, addmeth: AddMeth) -> None:
@@ -97,7 +97,7 @@ def he_method1_c() -> None:
9797
def he_method1_d() -> None:
9898
pass
9999

100-
assert funcs(hc._nonwrappers) == [
100+
assert funcs(hc.get_hookimpls()) == [
101101
he_method1_d,
102102
he_method1_b,
103103
he_method1_a,
@@ -118,7 +118,7 @@ def he_method1_b() -> None:
118118
def he_method1() -> None:
119119
pass
120120

121-
assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b]
121+
assert funcs(hc.get_hookimpls()) == [he_method1, he_method1_middle, he_method1_b]
122122

123123

124124
def test_adding_nonwrappers_tryfirst(hc: _HookCaller, addmeth: AddMeth) -> None:
@@ -134,7 +134,7 @@ def he_method1_middle() -> None:
134134
def he_method1_b() -> None:
135135
pass
136136

137-
assert funcs(hc._nonwrappers) == [he_method1_middle, he_method1_b, he_method1]
137+
assert funcs(hc.get_hookimpls()) == [he_method1_middle, he_method1_b, he_method1]
138138

139139

140140
def test_adding_wrappers_ordering(hc: _HookCaller, addmeth: AddMeth) -> None:
@@ -150,8 +150,11 @@ def he_method1_middle() -> None:
150150
def he_method3() -> None:
151151
pass
152152

153-
assert funcs(hc._nonwrappers) == [he_method1_middle]
154-
assert funcs(hc._wrappers) == [he_method1, he_method3]
153+
assert funcs(hc.get_hookimpls()) == [
154+
he_method1_middle,
155+
he_method1,
156+
he_method3,
157+
]
155158

156159

157160
def test_adding_wrappers_ordering_tryfirst(hc: _HookCaller, addmeth: AddMeth) -> None:
@@ -163,8 +166,116 @@ def he_method1() -> None:
163166
def he_method2() -> None:
164167
pass
165168

166-
assert hc._nonwrappers == []
167-
assert funcs(hc._wrappers) == [he_method2, he_method1]
169+
assert funcs(hc.get_hookimpls()) == [he_method2, he_method1]
170+
171+
172+
def test_adding_wrappers_complex(hc: _HookCaller, addmeth: AddMeth) -> None:
173+
assert funcs(hc.get_hookimpls()) == []
174+
175+
@addmeth(hookwrapper=True, trylast=True)
176+
def m1() -> None:
177+
...
178+
179+
assert funcs(hc.get_hookimpls()) == [m1]
180+
181+
@addmeth()
182+
def m2() -> None:
183+
...
184+
185+
assert funcs(hc.get_hookimpls()) == [m2, m1]
186+
187+
@addmeth(trylast=True)
188+
def m3() -> None:
189+
...
190+
191+
assert funcs(hc.get_hookimpls()) == [m3, m2, m1]
192+
193+
@addmeth(hookwrapper=True)
194+
def m4() -> None:
195+
...
196+
197+
assert funcs(hc.get_hookimpls()) == [m3, m2, m1, m4]
198+
199+
@addmeth(hookwrapper=True, tryfirst=True)
200+
def m5() -> None:
201+
...
202+
203+
assert funcs(hc.get_hookimpls()) == [m3, m2, m1, m4, m5]
204+
205+
@addmeth(tryfirst=True)
206+
def m6() -> None:
207+
...
208+
209+
assert funcs(hc.get_hookimpls()) == [m3, m2, m6, m1, m4, m5]
210+
211+
@addmeth()
212+
def m7() -> None:
213+
...
214+
215+
assert funcs(hc.get_hookimpls()) == [m3, m2, m7, m6, m1, m4, m5]
216+
217+
@addmeth(hookwrapper=True)
218+
def m8() -> None:
219+
...
220+
221+
assert funcs(hc.get_hookimpls()) == [m3, m2, m7, m6, m1, m4, m8, m5]
222+
223+
@addmeth(trylast=True)
224+
def m9() -> None:
225+
...
226+
227+
assert funcs(hc.get_hookimpls()) == [m9, m3, m2, m7, m6, m1, m4, m8, m5]
228+
229+
@addmeth(tryfirst=True)
230+
def m10() -> None:
231+
...
232+
233+
assert funcs(hc.get_hookimpls()) == [m9, m3, m2, m7, m6, m10, m1, m4, m8, m5]
234+
235+
@addmeth(hookwrapper=True, trylast=True)
236+
def m11() -> None:
237+
...
238+
239+
assert funcs(hc.get_hookimpls()) == [m9, m3, m2, m7, m6, m10, m11, m1, m4, m8, m5]
240+
241+
@addmeth(hookwrapper=True)
242+
def m12() -> None:
243+
...
244+
245+
assert funcs(hc.get_hookimpls()) == [
246+
m9,
247+
m3,
248+
m2,
249+
m7,
250+
m6,
251+
m10,
252+
m11,
253+
m1,
254+
m4,
255+
m8,
256+
m12,
257+
m5,
258+
]
259+
260+
@addmeth()
261+
def m13() -> None:
262+
...
263+
264+
assert funcs(hc.get_hookimpls()) == [
265+
m9,
266+
m3,
267+
m2,
268+
m7,
269+
m13,
270+
m6,
271+
m10,
272+
m11,
273+
m1,
274+
m4,
275+
m8,
276+
m12,
277+
m5,
278+
]
168279

169280

170281
def test_hookspec(pm: PluginManager) -> None:

0 commit comments

Comments
 (0)