Skip to content
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

Hook call mismatch warnings #42

Merged
merged 5 commits into from
Feb 25, 2017

Conversation

goodboy
Copy link
Contributor

@goodboy goodboy commented Feb 13, 2017

This add's proper warnings for hook calls which do not match the corresponding hookspec's signature. The idea is to encourage callers of hooks to always keep up to date with the most recent spec.

This comes from discussion in #15.

@hpk42 @nicoddemus @RonnyPfannschmidt this is the first part of #34 which I have separated out to simplify the review and narrow focus.

@goodboy
Copy link
Contributor Author

goodboy commented Feb 14, 2017

@hpk42 @nicoddemus @RonnyPfannschmidt any opinions on this?
I was hoping to move along sooner then later ;)

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @tgoodlet for your work!

@@ -581,7 +595,7 @@ def varnames(func):
try:
func = func.__init__
except AttributeError:
return ()
return (), ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should update the docs for this function given the new return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i just noticed something dreadful, since this is a public name, this is a breaking change would warrant a major incompatible release

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as an example, if we had used this in pytest and didn't do vendoring, this would be a release that breaks pytest

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt you got a point, we should increase the version to 0.5.0 and mention this change in the CHANGELOG.

In addition, should we make this method private and remove it from the public API?

Copy link
Member

@nicoddemus nicoddemus Feb 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaik this isn't "really" part of the public API due to the contents of pluggy.all?

Oh that's a good point.

tox doesn't use it as well, neither does devpi using a local search (BB doesn't seem to have a search feature).

I think we can safely rename it to an internal function to avoid confusion then. IMHO no need to provide a wrapper for it since nobody outside pluggy uses it as far as we know, and in case someone complains and makes a good case for it we can reintroduce it and promote to the public API.

0.X is the period of time we can break things if we think that's better. 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt if you agree then I won't bother

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do agree on practicality vs purity grounds

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt so do you still want the wrapper or no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgoodlet its not needed, i meant that i agree with @nicoddemus that on grounds of probably controlled set of users and pre 1.0 we can just make it private without going the extra mile

@@ -605,10 +623,10 @@ def varnames(func):

assert "self" not in args # best naming practises check?
try:
cache["_varnames"] = args
cache["_varnames"] = args, defaults
except TypeError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this code was here before already, but I'm curious in which situations can cache["_varnames"] raise a TypeError.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus yeah I never really understood the weird "storing arg names in the function's dict" in the first place. Can't we just have a single global cache which hashes by object id?

Maybe @hpk42 can comment?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgoodlet i remember that this optimization safes a few hops, and going for a cache on the object is very practical as it indeed ties object lifetime and cache lifetime

Copy link
Contributor Author

@goodboy goodboy Feb 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt huh that's an interesting point.

I've always understood that function lifetimes last from module import until interpreter exit unless you explicitly delete references to them. Also consider varnames() is only normally called once at PluginManager.register() time (really only once for each hookspec or hookimpl) so I might even go so far as to say this whole cache idea is a pre-mature optimization.

That being said @hpk42 had it there originally and I see no strong reason to remove it.

Oh and if we did end up wanting to use a global cache we could just use weakref.WeakValueDictionary and get the same gc behavior no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus btw do you think this is worth making a separate issue?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps writing a series of banchmarks so we could have a safe suite to test optimizations would be something worth exploring.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus agreed that's what #37 is for ;)

Maybe I'll hack on that next in an effort to rally some faith!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgoodlet for an weak dictionary there is a need to handle some things differently, unless im mistaken there will be a need to decompose to method objects, and it will create a new magical global managing lifetimes, personally i believe its much more safe to bind lifetimes to the objects directly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt I'm not sure it will matter but maybe there's something I'm missing. I do think we should leave it for now.

If you think I should open discussion about then I'll make a separate issue :)

notinspec = set(hookimpl.argnames) - set(hook.argnames)
if notinspec:
raise PluginValidationError(
"Plugin %r for hook %r\nhookimpl definition: %s\n"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression we should always use the term "arguments" or "keyword arguments" when conveying information to users, given that for all purposes hookimpls should always match the argument names of hookspecs, but position is not of importance. At least that's how pluggy and pytest hooks work in my mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus the only thing is if we're going to support actual keyword arguments as in #43, then shouldn't we distinguish?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't #43 about supporting default values?

Let's see if we are using the same nomenclature first. Considering this function:

def foobar(x, y, z=4):
    pass  
  • positional arguments: caller passes by position so order matters: foobar(1, 2, 10)
  • keyword arguments: caller passes using keyword syntax so order does not matter: foobar(y=2, x=1, z=10)
  • default arguments: caller does not need to pass them on: foobar(1, 2) # z = 4

As far as I understand, hooks are always called using keyword arguments so order is not an issue, that's why my comment was referring to "Positional arguments" as confusing.

Note that I'm talking about the caller here, not the definition. The definition might for example force the caller to call a function using only keywords:

def foobar(*, x, y, z=4):
    pass

(Python 3 only of course)

AFAIU, pluggy always calls using keyword arguments, so specs are implicitly defined as:

def pytest_deselectitems(*, items, session):
    pass

And that issue is completely separated from default values.

Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus no you're definitions are absolutely correct.
Sorry when I said "keyword" I meant to say "default".

So yes my concern is about being explicit with regard to hookimpl to hookspec mismatches regarding default arguments which is really not in the scope of this PR (it's coupled here because originally #43 and this were one PR).

I'll revert and thanks for clarifying.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh OK that explains it. 😄

Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus alright fixed. Let me know if there's anything else otherwise I'll squash the history.

pluggy.py Outdated
notincall = set(self.argnames) - set(kwargs.keys())
if notincall:
warnings.warn(
"Positional arg(s) %s are declared in the hookspec "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about "Positional arg" I made in my previous comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one I agree with since we only allow calling with "named argument" syntax currently.

@@ -13,16 +13,16 @@ class B(object):
def __call__(self, z):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to test the generated warnings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes good call 👍

@goodboy
Copy link
Contributor Author

goodboy commented Feb 16, 2017

@nicoddemus fixed up most of it and added the test. Let me know what you think.

Tyler Goodlet added 4 commits February 18, 2017 10:44
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
- 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
@goodboy
Copy link
Contributor Author

goodboy commented Feb 18, 2017

@hpk42 @nicoddemus rebased after #46.

I think this should be ready. Is there any more issues you guys want me to sort out?

@hpk42 you've been a bit silent :(

@goodboy
Copy link
Contributor Author

goodboy commented Feb 22, 2017

@nicoddemus @hpk42 anything else you guys aren't happy with?

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I think @hpk42 or @RonnyPfannschmidt should merge it as they know pluggy in more depth than me.

@RonnyPfannschmidt
Copy link
Member

@nicoddemus i believe this one can be merged, but we will absolutely have to put that wrapper/rename in place before the next release

@tgoodlet if you get that in before @nicoddemus chimes in i can also just merge

in any case good work and thanks for staying on top of this "never-ending story" 👍

@goodboy
Copy link
Contributor Author

goodboy commented Feb 24, 2017

@RonnyPfannschmidt yes I'll get the wrapper in today 👍
Can you also look at some of my other responses ^ I just want to make sure we're on the same page :)

And thanks for the thanks!

@goodboy
Copy link
Contributor Author

goodboy commented Feb 25, 2017

@RonnyPfannschmidt @nicoddemus ok I'll squash the history and merge this once I get back later tonight if you guys both give the thumbs up!

@nicoddemus
Copy link
Member

👍 please go ahead!

@nicoddemus nicoddemus merged commit 26ddfe4 into pytest-dev:master Feb 25, 2017
@nicoddemus
Copy link
Member

Squashed and merged. 👍

Thanks again @tgoodlet, excellent work!

@goodboy
Copy link
Contributor Author

goodboy commented Feb 25, 2017

@nicoddemus my pleasure :)

@goodboy
Copy link
Contributor Author

goodboy commented Feb 25, 2017

@nicoddemus huh weird how that doesn't look like a merge in the git history?
Almost looks like it was put directly on master?

@nicoddemus
Copy link
Member

I did use the "Squash and merge" option in GH's UI, I thought that's what we wanted since there were quite a few interim commits in this PR... if we wanted a normal merge I apologize.

@RonnyPfannschmidt
Copy link
Member

@nicoddemus i think we should avoid squashes in future, my experience with them has been surprising and frustrating as well

@nicoddemus
Copy link
Member

No problem @RonnyPfannschmidt, my mistake!

@goodboy
Copy link
Contributor Author

goodboy commented Mar 3, 2017

@nicoddemus @RonnyPfannschmidt yeah I when I said "squashed" I practically meant I would git rebase -i into less commits; not a literal GH "squash".

I can break this history up into the two commits I had in mind (impl and test) if you guys don't me rewriting history on master? It shouldn't affect anyone given there was no release made and I'm the only one hacking atm.

@RonnyPfannschmidt
Copy link
Member

hmm, never ever rewrite history on master ^^ we can undo the pr and accept a new one, but rewriting master is a absolute no go

@nicoddemus
Copy link
Member

never ever rewrite history on master

I agree... let's just accept this as a mistake and move on. 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants