-
Notifications
You must be signed in to change notification settings - Fork 590
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
rename target arg so we can use targets requiring an argument 'target' #1106
rename target arg so we can use targets requiring an argument 'target' #1106
Conversation
Also:
|
Hi @dchudz - thanks for contributing 🎉 We're always delighted to help someone with a PR, be it the first or hundredth!
The def builds(*target_and_args, **kwargs):
if target_and_args:
target, *args = target_and_args
elif 'target' in kwargs:
note_deprecation(...) # deprecated, give target as the first posarg, etc
target = kwargs.pop('target')
else:
raise InvalidArgument(...) # target must be given as the first posarg, etc
... This avoids adding a new keyword argument, which was changing the repr - and also making the problem less common instead of fixing it ("I can't build things with a keyword only arg named ..."). The downside is that this makes the signature a bit less useful, but IMO it's a net improvement if we also use a descriptive name. Possible that @DRMacIver will veto this API though, so if your time is at a premium you might want to let him chime in before implementing. |
Yeah, I was tempted by this, but pushed away by the less useful signature and @DRMacIver's example code in the issue doing it this way. (That said, the argument name I like your approach - I'll do that tomorrow night if I haven't seen a veto here first.
The only frustrating thing was having trouble running just a single unit test (b/c of the test-specific dependencies). (I eventually deleted
The docs were fun to read. :) (Reading them with my morning coffee was how I got interested enough to decide to see if I could find an issue easy enough for me.) I spent a little while being confused by |
Oh, I guess you mean you don't want errors when a user's module is imported. (For some reason I thought you meant the import of I guess I'm still a bit confused by that, though. If you know I wrote broken test code even before you run any tests, I feel like I'd prefer to be told then. But I realize I'm missing tons of context, and of course it's fine if you don't have time to help me get it. :) |
There's going to be a note about test dependencies, and The preference for errors at runtime instead of import time is purely because it allows other tests to run, and therefore pass or fail for other reasons - useful if you're eg. selecting only unrelated tests to run. We want Hypothesis to be all correct all the time, but not everyone shares that view and better a failing test than no test! This is definitely a subjective decision that could reasonably go the other way, FWIW.
😍 Docs so good that they actually attract contributions?! 😍
|
Thanks for this seconded @dchudz! Ugh, sorry about the repr issue. That's an annoying thing that I didn't think of when I proposed my original solution. My bad. I think @Zac-HD's suggestion of putting it in That being said, I don't think the suggested code is quite right. As Agreed this makes the signature a bit ugly, but I don't think we have a way out of that right now. Long term I think the right thing to do is to get a bit more flexibility in how we display those reprs - currently it treats it as if it's always more sensible to show arguments in their keyword form. This is so that something like e.g.
There are a couple of reasons we prefer to only throw errors at test execution time. The big ones are:
(This should probably go in the guides and maybe public documentation somewhere, as I agree this is non-intuitive). |
Additional questions and answers:
Does
I think it might be OK not to given that it calls
Agreed. I don't think it makes sense to add docstrings to nested functions (especially ones that will inherit them from the wrapped function!) but the actual decorator should definitely have a docstring. |
Builds may accept any callable, not just a function (eg a class); otherwise well spotted - I (obviously) missed that bug 😥
#1100 again? In this @dchudz if you're adding docstrings, you could also switch over the decorator on |
I'm tempted to object on grounds of scope creep, but decided it would be easier to just do it. 😉 This is the last addition though! |
Whoops. True, yes. I think what I said still applies though - strategies that we define are never callable, and users can't define their on strategy subclasses, so there's still no overlap. |
Wow, thanks for all the answers/explanation! After work today I'll fix this up and also open a PR with @Zac-HD's suggested docstrings for
I kind of picked the first one in the list, but also the tags (
Ugh, oops/sorry. Silly me. I even saw
I don't think so, my mistake. I really meant to suggest |
@DRMacIver Note that |
Despite the ugly signature, |
|
ds.builds().example() | ||
|
||
|
||
@pytest.mark.parametrize('non_callable', [1, 'abc', ds.integers()]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this could be a hypothesis-style randomized test? But writing a strategy for "non-callable" didn't really seem worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parametrize is fine here - Hypothesis is generally better at "works for all valid inputs" than "fails for all invalid inputs"; the latter space is enormous and mostly trivial anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! This is looking good, and I really like the tests 😀
I've left fairly detailed comments about style, and one substantive change to the error you'd get if no posarg is provided. Fix those, and I'll be delighted to merge it in 🎉
RELEASE.rst
Outdated
@@ -0,0 +1,3 @@ | |||
RELEASE_TYPE: patch | |||
|
|||
This release fixes :func:`builds(callable) <hypothesis.strategies.builds>` so that ``target`` can be used as a keyword argument for passing values to the target. The target itself can still be specified as a keyword argument, but that behavior is now deprecated. The target should be provided as the first positional argument. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our convention for strategies is to use the default link text for all but decorators, so just
:func:`~hypothesis.strategies.builds`
Wrap lines at 79 characters - the old convention is still useful for diffs etc., and we enforce it for the code (see the lint
job on Travis).
src/hypothesis/strategies.py
Outdated
@@ -945,11 +955,26 @@ def builds(target, *args, **kwargs): | |||
the target. | |||
|
|||
""" | |||
if target_and_args: | |||
target, args = target_and_args[0], target_and_args[1:] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would write this target, *args = target_and_args
- same effect and considerably shorter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, doesn't work in Python2. Maybe in 2020? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't come soon enough 😭
src/hypothesis/strategies.py
Outdated
if target_and_args: | ||
target, args = target_and_args[0], target_and_args[1:] | ||
if not callable(target): | ||
raise InvalidArgument('The first positional argument to builds() must be a callable' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs a space at the end of this string, to avoid joining words when it's concatenated.
src/hypothesis/strategies.py
Outdated
if infer in args: | ||
# Avoid an implementation nightmare juggling tuples and worse things | ||
raise InvalidArgument('infer was passed as a positional argument to ' | ||
'builds(), but is only allowed as a keyword arg') | ||
hints = get_type_hints(target.__init__ if isclass(target) else target) | ||
hints = get_type_hints(target.__init__ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to change this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh right, sorry, that was for line length in the older version that introduced the longer name hypothesis_internal_target
src/hypothesis/strategies.py
Outdated
if not callable(target): | ||
raise InvalidArgument('The first positional argument to builds() must be a callable' | ||
'target to construct.') | ||
elif 'target' in kwargs: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
elif 'target' in kwargs and callable(kwargs['target']):
Handles the case where someone passed target=some_strategy
but did not supply any positional arguments - we want that to be the generic "no callable supplied" error, and weren't checking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I'll also add a test that would have failed w/o this fix.
src/hypothesis/strategies.py
Outdated
'Provide it as the first positional argument instead.') | ||
target = kwargs.pop('target') | ||
else: | ||
raise InvalidArgument('No target was provided to builds().' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No space at end of string, and I'd prefer to avoid calling the callable "target". How about: "builds() must be passed a callable as the first positional argument, but no positional argument was given."
As a general tip, for longer strings we generally start them on the line after the opening parenthesis, indented by four spaces. There are some examples in eg. decimals()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to avoid calling the callable "target".
Makes sense. Unless you object, I'm still referring to 'target' in the deprecation message ("Specifying the target as a keyword argument to builds() is deprecated ...") since in that case the user just called it target
so that naming is familiar to them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I agree that it should be kept in the deprecation message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll clean up some other references to "target" I accidentally left in too.
@@ -225,6 +227,29 @@ def test_produces_valid_examples_from_args(fn, args): | |||
fn(*args).example() | |||
|
|||
|
|||
def test_build_class_with_target_kwarg(): | |||
NamedTupleWithTargetField = collections.namedtuple('Something', ['target']) | |||
ds.builds(NamedTupleWithTargetField, target=ds.integers()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to add a .example()
on the end, or there could be a lazy error lurking 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, right!
ds.builds().example() | ||
|
||
|
||
@pytest.mark.parametrize('non_callable', [1, 'abc', ds.integers()]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parametrize is fine here - Hypothesis is generally better at "works for all valid inputs" than "fails for all invalid inputs"; the latter space is enormous and mostly trivial anyway.
More detailed summary of API changes in case this helps anyone:
|
Thanks for the great comments, @Zac-HD! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI InvalidArgument
inherits from TypeError
, so except TypeError
or with pytest.raises(TypeError)
will still catch it - as will trying to handle the top-level HypothesisException
😄
Thanks for the great comments
😊. Thanks for the great PR!
Final (I think) round of comments: some trailing whitespace, and a last rename:
src/hypothesis/strategies.py
Outdated
@@ -928,23 +937,41 @@ def do_draw(self, data): | |||
|
|||
@cacheable | |||
@defines_strategy | |||
def builds(target, *args, **kwargs): | |||
def builds(*target_and_args, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It now occurs to me that we should probably name this argument *callable_and_args
, to banish the word "target" entirely. Sorry 😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, makes sense.
RELEASE.rst
Outdated
@@ -0,0 +1,6 @@ | |||
RELEASE_TYPE: patch | |||
|
|||
This release fixes :func:`~hypothesis.strategies.builds` so that ``target`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove trailing whitespace here. (thanks, linter)
oh right, thanks! |
This looks good to me, but I'll give @DRMacIver a chance to review it instead of merging (and releasing!) right away. In particular, he might want a minor rather than a patch release (I'm happy with a patch) - on one hand it's a bugfix and backwards-compatible; on the other there is a slight change to when errors are raised. |
I actually hadn't thought of it, but now that you mention it I actually think I do! I've added a ticket to pin down what the actual rules are around this (#1109), but in particular I think we shouldn't change the signature of public facing APIs in patch releases even in this very compatible way. |
Whoops. Good point. Totally agreed with your call. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some comments inline, but generally LGTM. 🎉 Thanks for the patch! Once the minor/patch thing is addressed (and the wording comment I added if you care), let us know you're done and one of us will happily merge!
src/hypothesis/strategies.py
Outdated
else: | ||
raise InvalidArgument( | ||
'builds() must be passed a callable as the first positional ' | ||
'argument, but no positional argument was given.') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super minor nit: "no positional arguments were given".
RELEASE.rst
Outdated
@@ -0,0 +1,6 @@ | |||
RELEASE_TYPE: patch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per discussion in comments, I think this needs to be a minor release rather than a patch.
Hmm, the failing job says Seems unlikely this is really related to my PR. Help...? |
Yeah, sorry, Travis can get a bit flaky when there are as many build jobs as we have and it's otherwise under load! Sorry about that. I've restarted the failing job and expect it will go green, as it's unlikely in the extreme that this is anything you did. 😄 Will click merge once it's done! |
🎉 |
Yay! |
Sorry our amazing auto-deployment system is being less than amazing due to Travis flakiness. :-( I've tried restarting the failing bits of the build once already and it failed again. Trying once more... Should hopefully ship it soon! |
And, it's released! Thanks again @dchudz and congratulations 🎉✨ |
This is my attempt to address #1104, following the suggestion in the comment from @DRMacIver on that issue.
But I don't think it's good:
builds()
with no argument for the target no longer raisesTypeError
(builds().example()
does), since the new code here isn't actually run when you callbuilds()
builds()
I'm not thinking of any ways to fix this without adding a bunch of complication (I'd have to mess with
proxies()
and/orbase_defines_strategy()
?).I'm completely new here, but it seems to me that merging this would make life worse for users and developers.
I figured I'd post it anyway, since (a) maybe I'm wrong about that or (b) I'd love to be pointed in a better direction if anyone has time to set me right.
Or if not, we can just close this and add to the issue: "Don't fix it THAT way!"