Skip to content

Commit 44b386c

Browse files
author
Tyler Goodlet
committed
Encapsulate spec definitions with a class
Allows for easier introspection of spec definitions including function signatures and hook options. Originally introduced to address pytest-dev#15 and the accompanying PR (pytest-dev#43) which requires keeping track of spec default arguments values.
1 parent 76e8aba commit 44b386c

File tree

4 files changed

+38
-28
lines changed

4 files changed

+38
-28
lines changed

pluggy.py

+33-23
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,12 @@ def __init__(self, project_name, implprefix=None):
274274
self.trace = _TagTracer().get("pluginmanage")
275275
self.hook = _HookRelay(self.trace.root.get("hook"))
276276
self._implprefix = implprefix
277-
self._inner_hookexec = lambda hook, methods, kwargs: \
278-
_MultiCall(
279-
methods, kwargs, specopts=hook.spec_opts, hook=hook
280-
).execute()
277+
self._inner_hookexec = lambda hook, methods, kwargs: _MultiCall(
278+
methods,
279+
kwargs,
280+
firstresult=hook.spec.opts['firstresult'] if hook.spec else False,
281+
hook=hook
282+
).execute()
281283

282284
def _hookexec(self, hook, methods, kwargs):
283285
# called from all hookcaller instances.
@@ -424,7 +426,7 @@ def _verify_hook(self, hook, hookimpl):
424426
(hookimpl.plugin_name, hook.name))
425427

426428
# positional arg checking
427-
notinspec = set(hookimpl.argnames) - set(hook.argnames)
429+
notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
428430
if notinspec:
429431
raise PluginValidationError(
430432
"Plugin %r for hook %r\nhookimpl definition: %s\n"
@@ -517,8 +519,8 @@ def subset_hook_caller(self, name, remove_plugins):
517519
orig = getattr(self.hook, name)
518520
plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
519521
if plugins_to_remove:
520-
hc = _HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class,
521-
orig.spec_opts)
522+
hc = _HookCaller(orig.name, orig._hookexec, orig.spec.namespace,
523+
orig.spec.opts)
522524
for hookimpl in (orig._wrappers + orig._nonwrappers):
523525
plugin = hookimpl.plugin
524526
if plugin not in plugins_to_remove:
@@ -539,17 +541,18 @@ class _MultiCall(object):
539541
# so we can remove it soon, allowing to avoid the below recursion
540542
# in execute() and simplify/speed up the execute loop.
541543

542-
def __init__(self, hook_impls, kwargs, specopts={}, hook=None):
543-
self.hook = hook
544+
def __init__(self, hook_impls, kwargs, firstresult=False, hook=None):
544545
self.hook_impls = hook_impls
545546
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
546547
self.caller_kwargs["__multicall__"] = self
547-
self.specopts = hook.spec_opts if hook else specopts
548+
self.firstresult = firstresult
549+
self.hook = hook
550+
self.spec = hook.spec if hook else None
548551

549552
def execute(self):
550553
caller_kwargs = self.caller_kwargs
551554
self.results = results = []
552-
firstresult = self.specopts.get("firstresult")
555+
firstresult = self.firstresult
553556

554557
while self.hook_impls:
555558
hook_impl = self.hook_impls.pop()
@@ -560,8 +563,10 @@ def execute(self):
560563
if argname not in caller_kwargs:
561564
raise HookCallError(
562565
"hook call must provide argument %r" % (argname,))
566+
563567
if hook_impl.hookwrapper:
564568
return _wrapped_call(hook_impl.function(*args), self.execute)
569+
565570
res = hook_impl.function(*args)
566571
if res is not None:
567572
if firstresult:
@@ -645,28 +650,23 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None)
645650
self._wrappers = []
646651
self._nonwrappers = []
647652
self._hookexec = hook_execute
648-
self.argnames = None
649-
self.kwargnames = None
653+
self.spec = None
654+
self._call_history = None
650655
if specmodule_or_class is not None:
651656
assert spec_opts is not None
652657
self.set_specification(specmodule_or_class, spec_opts)
653658

654659
def has_spec(self):
655-
return hasattr(self, "_specmodule_or_class")
660+
return self.spec is not None
656661

657662
def set_specification(self, specmodule_or_class, spec_opts):
658663
assert not self.has_spec()
659-
self._specmodule_or_class = specmodule_or_class
660-
specfunc = getattr(specmodule_or_class, self.name)
661-
# get spec arg signature
662-
argnames, self.kwargnames = varnames(specfunc)
663-
self.argnames = ["__multicall__"] + list(argnames)
664-
self.spec_opts = spec_opts
664+
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
665665
if spec_opts.get("historic"):
666666
self._call_history = []
667667

668668
def is_historic(self):
669-
return hasattr(self, "_call_history")
669+
return self._call_history is not None
670670

671671
def _remove_plugin(self, plugin):
672672
def remove(wrappers):
@@ -702,8 +702,8 @@ def __repr__(self):
702702

703703
def __call__(self, **kwargs):
704704
assert not self.is_historic()
705-
if self.argnames:
706-
notincall = set(self.argnames) - set(['__multicall__']) - set(
705+
if self.spec:
706+
notincall = set(self.spec.argnames) - set(['__multicall__']) - set(
707707
kwargs.keys())
708708
if notincall:
709709
warnings.warn(
@@ -749,6 +749,16 @@ def _maybe_apply_history(self, method):
749749
proc(res[0])
750750

751751

752+
class HookSpec(object):
753+
def __init__(self, namespace, name, opts):
754+
self.namespace = namespace
755+
self.function = function = getattr(namespace, name)
756+
self.name = name
757+
self.argnames, self.kwargnames = varnames(function)
758+
self.opts = opts
759+
self.argnames = ["__multicall__"] + list(self.argnames)
760+
761+
752762
class HookImpl(object):
753763
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
754764
self.function = function

testing/benchmark.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def MC(methods, kwargs, firstresult=False):
1313
for method in methods:
1414
f = HookImpl(None, "<temp>", method, method.example_impl)
1515
hookfuncs.append(f)
16-
return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult})
16+
return _MultiCall(hookfuncs, kwargs, firstresult=firstresult)
1717

1818

1919
@hookimpl

testing/test_method_ordering.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,9 @@ def he_myhook3(arg1):
163163
pass
164164

165165
pm.add_hookspecs(HookSpec)
166-
assert not pm.hook.he_myhook1.spec_opts["firstresult"]
167-
assert pm.hook.he_myhook2.spec_opts["firstresult"]
168-
assert not pm.hook.he_myhook3.spec_opts["firstresult"]
166+
assert not pm.hook.he_myhook1.spec.opts["firstresult"]
167+
assert pm.hook.he_myhook2.spec.opts["firstresult"]
168+
assert not pm.hook.he_myhook3.spec.opts["firstresult"]
169169

170170

171171
@pytest.mark.parametrize('name', ["hookwrapper", "optionalhook", "tryfirst", "trylast"])

testing/test_multicall.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def MC(methods, kwargs, firstresult=False):
2222
for method in methods:
2323
f = HookImpl(None, "<temp>", method, method.example_impl)
2424
hookfuncs.append(f)
25-
return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult})
25+
return _MultiCall(hookfuncs, kwargs, firstresult=firstresult)
2626

2727

2828
def test_call_passing():

0 commit comments

Comments
 (0)