-
Notifications
You must be signed in to change notification settings - Fork 126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support defaults to enable deprecation #15
Comments
Usually a project defining a hookspec is also calling it. So this is basically a very minor convenience way of expressing a default and it blurs the line between what is a spec and what is implementation. I think it's clearer to request from a hook calling site to provide all arguments and not have to worry cross-checking with the spec on what parameters are actually passed to impls. |
@hpk - this is directly broken for how we document pytest_deselected atm its missing a actually sane way to transfer the reason |
What exactly is broken? What is not working as expected? At the moment we don't promise that we do anything with default arguments in specs. They are currently ignored -- we could throw an error to be more explicit. I hold providing call-kwargs values through a hookspec is confusing. |
@hpk42 the underlying problem is the following
if we now want to add a |
ah, now i get it, thanks. So this is a situation where callers of a hook live in separate projects and thus changing the spec becomes hard. This kind of trumps (!) my concern then i guess. Feel free to submit a PR. if you hurry it might make it into pluggy-0.5. |
@hpk42 pretty sure I can nail out this one :) |
On Fri, Nov 11, 2016 at 07:04 -0800, goodboy wrote:
@nicoddemus reminded me that pytest actually has its own vendored version of pluggy. If that is true also for older pytest versions (i think it is) we don't actually need to support multicall neccessarily anymore. It might mean, though, that pytest only wants to go for vendoring a non-multicall pluggy for pytest-4.0. Which in turns means we might want to support it for a while still at best through an opt-in parameter to instantiating a PluginManager. |
@tgoodlet this one might be a bit tricky |
@RonnyPfannschmidt just to make sure I understand this correctly: hookspec = HookspecMarker("prefix")
hookimpl = HookimplMarker("prefix")
class Spec:
@hookspec
def prefix_myhook(self, doggy, kitty='mean'):
pass
@hookimpl
def prefix_myhook(doggy, kitty):
assert doggy is not 'smelly', "Kick out the dog"
print("shocker kitty is always {}".format(kitty))
@hookimpl
def prefix_myhook(doggy, kitty='smelly'):
assert doggy is not 'smelly', "Kick out the dog"
print("shocker kitty is always {}".format(kitty))
pm = PluginManger('prefix')
pm.add_hookspec(Spec)
pm.register(sys.modules[__name__])
>>> pm.hook.prefix_myhook(doggy='just bathed')
"shocker kitty is always mean" # from the first impl
"shocker kitty is always smelly" # from the second impl
>>> pm.hook.prefix_myhook(doggy='just bathed', kitty='grumpy')
"shocker kitty is always grumpy" # from the first impl
"shocker kitty is always grumpy" # from the second impl |
uh, that precedence questions highlights that allowing default args in specs is tricky. if anything, i'd argue that the impl's default should take precedence over the spec's default. I am afraid however we decide about precedence, it's going to be surprising to some and you have to check three places to understand where a value comes from ... sorry, ronny, i am getting unsure again if this is the right approach to solve the underlying problem we discussed earlier: callers of a hook become incompatible if an argument is added to the spec. I'd now like to get others to comment on the problem before you tackle impl, tests, docs. My view is that pluggy is foremost about 1:N calls, not sure much about M:N calls (with M = numb of the different places where a call is made). On a side note, maybe this pytest deselect hook calling is a bit of a bad idea anyway. pytest's core collection hook should automatically call it by looking at pre-post list of items. Or it should even all be just done by the terminal reporter. This way user code is not concerned with it at all. |
My initial reaction was the opposite, which only emphasizes your point about this being tricky.
FWIW, I always thought it a bit odd... it felt weird calling a core hook from a plugin implementation.
The same thought occurred to me when I had to call Do we have other hooks with the same behavior as If there's no other cases, we might instead consider just getting rid of the odd child that is |
@hpk42 just want to clarify the 3 places in reverse precedence order - spec, impl, call. I guess your concern is with debugging? As far as I understand the implementer of a hook shouldn't be too concerned about where a value came from.
@hpk42 I'm not sure this is true as long as the new argument is a kwarg. In fact that's usually the beauty of one. As an example I already have a working draft patch of def execute(self):
- all_kwargs = self.kwargs
+ all_kwargs = copy.copy(self.kwargs)
self.results = results = []
firstresult = self.specopts.get("firstresult")
while self.hook_impls:
hook_impl = self.hook_impls.pop()
+ if "__multicall__" in hook_impl.argnames:
+ all_kwargs["__multicall__"] = self
try:
- args = [all_kwargs[argname] for argname in hook_impl.argnames]
+ args = [all_kwargs.pop(argname) for argname in hook_impl.argnames]
except KeyError:
for argname in hook_impl.argnames:
- if argname not in all_kwargs:
+ if argname not in self.kwargs:
raise HookCallError(
"hook call must provide argument %r" % (argname,))
if hook_impl.hookwrapper:
- return _wrapped_call(hook_impl.function(*args), self.execute)
- res = hook_impl.function(*args)
+ return _wrapped_call(hook_impl.function(*args, **all_kwargs),
+ self.execute)
+ res = hook_impl.function(*args, **all_kwargs)
if res is not None:
if firstresult:
return res
Fwiw M calls turns out to be very useful for event like systems made with |
my current oppinion is that spec defaults should always win over impl defaults a hookimpl should be able to declare the default for backward compatibility (say a hook adds a new parameter you want to use if availiable, but still support older versions of the program that did not support/supply it @hpk42 the items object for the modifyitems hook could have a richer api than just being a normal list |
@tgoodlet if i write an impl like IOW, i suggest we consider only allowing None default values for specs and maybe also for impls. Or find some other way to cater the case of wanting to have callers with different nums of arguments work. |
@hpk42 the idea to limit the defaults only to if this is documented as intentionally restricted non-neogiable way to ensure backward/forward compatibility for call sites and hook implementations it should be fine |
@RonnyPfannschmidt Can't we accomplish this and what @hpk42 thinks the precedence order should be by simply not strictly requiring In other words, in From the
As an example of a @hookimpl
def myhookimpl(required_arg1, required_arg2, new_maybe_provided=None, **kwargs):
# must process the required args
do_something(arg1)
do_something(arg2)
if new_maybe_provided is not None:
# new enough version that this kwarg is provided and defined in the spec
do_something(new_maybe_provided)
# call to hook may have passed in additional data not yet defined in the spec
if 'my_new_kwarg' in kwargs:
do_something(kwargs['my_new_kwarg'])
... But, an older hook which declares no @hookimpl
def myhookimpl(required_arg1, required_arg2):
# must process the required args
do_something(arg1)
do_something(arg2)
# this still works since kwargs are opt in
myhookimpl(required_arg1='blah', required_arg2='doggy', new_maybe_provided=10, my_new_kwarg='yey') The spec would look like: @hookspec
def myhookimpl(required_arg1, required_arg2, new_maybe_provided=None):
'''A hook that requires args 1 and 2 and a new_maybe_provided kwarg value
''' This ^ allows us to transition new arguments into hooks by first including them as optional ... takes a breath ...
@hpk42 yes I totally agree with you about the precedence rules. However look above ^. I do think we could make
@hpk42 also totally agree.
@hpk42 @RonnyPfannschmidt my only question with this approach is how we transition in new arguments gracefully and with warnings about upcoming signature changes to old |
Arbitrary values make the system much harder to understand Kwargs where abadonded because they have a massive performance toll Your proposal grows in complexity, one of the reasons I quickly took the restriction to none is that it decouples many concerns from pluggey removing a number of mental burdens |
@RonnyPfannschmidt yes thinking about it more I agree. The whole point of the spec to to not allow arbitrary argument passing... @RonnyPfannschmidt @hpk42 So then are you guys cool with my proposal to pre-inspect |
@tgoodlet im not clearly understanding your differenciation wrt arguments vs kwargs currently pluggy thinks in terms of arguments, we use names in hook invocation, but the call happens in terms of position arguments for efficiency warnings for unknown ones ones or missing ones seems really helpfull in any case |
@RonnyPfannschmidt right but aren't we're discussing allowing As far as I understand this is to distinguish between newly introduced hook arguments and originally spec-ed arguments such that we can introduce new arguments to a hook spec without breaking legacy Further, if we keep hook calls using positional arguments and offer up and coming new arguments through
@RonnyPfannschmidt I find this slightly ironic considering we literally loop through all the provided @hpk42 I'm interested in your opinion on this performance comment ^ because I'm assuming maybe you originally had a good reason for doing it this way? |
@tgoodlet the transformation to positional arguments was introduced to gain performance (and did work out) i vaguely recall the issue that filtering down the dicts from all arguments to functions arguments was needed in any case (@hpk42 might recall more details) for the sake of mental models its useful to pretend that pluggy works solely in terms of keyword arguments |
@RonnyPfannschmidt that I don't doubt. I just don't know how much faster the list comprehension + Here's some very rudimentary benchmarks of what's going on inside In [19]: kwargs= {'doggy':10, 'kitty':'yey', 'mouse':1908}
In [20]: def f(kitty, doggy, mouse=None):
...: pass
...:
In [22]: fargnames = pluggy.varnames(f)
In [23]: fargnames
Out[23]: ('kitty', 'doggy')
# the current implementation
In [24]: timeit f(*[kwargs[argname] for argname in fargnames])
The slowest run took 4.67 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 556 ns per loop
In [26]: timeit f(**kwargs)
The slowest run took 6.64 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 211 ns per loop So if we're going to keep the call by keyword argument names interface then we might gain some performance by using |
@tgoodlet unfortunately you forgot to filter by applicable argument names (thats where the cost was) |
(it might have been better to cythonize, but that would mess up vendoring in pytest) |
@RonnyPfannschmidt ok that's another thing. Why do we allow that - Either way we still need to address how to handle
|
hooks only take arguments they need, that makes them forward compatible to the addition of new ones and removes unneeded variables from the function for example most hooks even in pytest don't take all arguments |
Ahhh so @RonnyPfannschmidt in that case then I'm not totally following how you guys think this can be accomplished using |
let me line up the cases to consider
the hook spec side is about supplying defaults in order to ensure forward compatibility for callers that pass less arguments the hook impl side is about supplying defaults for keeping compatibility with older versions of the hookspec that did not have the argument yet |
Add support for using declared keyword argument values from both hookspecs and hookimpls. The current logic will inspect the hookimpl and, if it contains defaults, values will be first looked up from the caller provided data and if not defined will be taken from the hookspec's declared defaults. If the spec does not define defaults the value is taken from the hookimpl's defaults as is expected under normal function call semantics. Resolves pytest-dev#15
Warn when either a hook call doesn't match a hookspec. Additionally, - Extend `varnames()` to return the list of both the arg and kwarg names for a function - Rename `_MultiCall.kwargs` to `caller_kwargs` to be more explicit - Store hookspec kwargs on `_HookRelay` and `HookImpl` instances Relates to pytest-dev#15
Add support for using declared keyword argument values from both hookspecs and hookimpls. The current logic will inspect the hookimpl and, if it contains defaults, values will be first looked up from the caller provided data and if not defined will be taken from the hookspec's declared defaults. If the spec does not define defaults the value is taken from the hookimpl's defaults as is expected under normal function call semantics. Resolves pytest-dev#15
Add support for using declared keyword argument values from both hookspecs and hookimpls. The current logic will inspect the hookimpl and, if it contains defaults, values will be first looked up from the caller provided data and if not defined will be taken from the hookspec's declared defaults. If the spec does not define defaults the value is taken from the hookimpl's defaults as is expected under normal function call semantics. Resolves pytest-dev#15
* Future warn about hookspec vs. call mis-matches Warn when either a hook call doesn't match a hookspec. Additionally, - Extend `varnames()` to return the list of both the arg and kwarg names for a function - Rename `_MultiCall.kwargs` to `caller_kwargs` to be more explicit - Store hookspec kwargs on `_HookRelay` and `HookImpl` instances Relates to #15 * Port tests to match new `varnames()` return value * Handle py2.6 and better docs - don't use "Positional" in warning message - support python 2.6 sets - don't include __multicall__ in args comparison - document `varnames()` new pair return value * Add a call vs. spec mismatch warning test * Drop "Positional" adjective
Add support for using declared keyword argument values from both hookspecs and hookimpls. The current logic will inspect the hookimpl and, if it contains defaults, values will be first looked up from the caller provided data and if not defined will be taken from the hookspec's declared defaults. If the spec does not define defaults the value is taken from the hookimpl's defaults as is expected under normal function call semantics. Resolves pytest-dev#15
Add support for using declared keyword argument values from both hookspecs and hookimpls. The current logic will inspect the hookimpl and, if it contains defaults, values will be first looked up from the caller provided data and if not defined will be taken from the hookspec's declared defaults. If the spec does not define defaults the value is taken from the hookimpl's defaults as is expected under normal function call semantics. Resolves pytest-dev#15
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.
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.
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.
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.
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.
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.
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.
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.
closing this one, i decided new hook names should be the way to go |
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.
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.
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.
this is to help pytest-dev/pytest#1372
the idea is, that if a hook is invoked with a missing, but specced argument, then the default is used instead
so if a hook caller would call
hook.pytest_deselected(items=...)' and the hook was defined with
def pytest_deselected(items, reason='No Reason Given)`then the receivers would receive the defined default reason
in order to support deprecation of such usage we could later devise a mark for deprecated defaults
The text was updated successfully, but these errors were encountered: