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
bpo-42109: Implementation of hypothesis stubs for property-based tests, with zoneinfo tests #22863
base: main
Are you sure you want to change the base?
Conversation
This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 5 days |
These are stubs to be used for adding hypothesis (https://hypothesis.readthedocs.io/en/latest/) tests to the standard library. When the tests are run in an environment where `hypothesis` and its various dependencies are not installed, the stubs will turn any tests with examples into simple parameterized tests and any tests without examples are skipped.
This migrates the tests from https://github.com/Zac-HD/stdlib-property-tests into the standard library, using the hypothesis stubs.
raise unittest.SkipTest("No time zone data available") | ||
|
||
|
||
def valid_keys(): |
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.
These tests were implemented before this, and the latest version of hypothesis
has a .timezone_keys
strategy as well as a .timezones
strategy, so we should probably refactor this, but there's no rush.
2 PRs in 1. Do you consider the hypothesis_helper part to be finished and ready to merge?
I intend to add more on the issue later.
from . import strategies | ||
|
||
|
||
def given(*_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.
Why the underscores?
Functions need docstrings. This one should mentions that arguments are not used as randomized strategies are not run.
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.
The underscores are because it's unused. Might be Google-isms or pylint-isms getting to me, because pylint won't complain about unused arguments if you start them with _.
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.
OK, never seen this. It might or might not be an issue for others.
# If we have found no examples, we must skip the test. If @example | ||
# is applied after @given, it will re-wrap the test to remove the | ||
# skip decorator. |
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.
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.
Hypothesis allows you to do stuff like:
@example(3)
@given(i=strategies.integers())
@example(4)
def test_i(i):
...
It's easy enough to support that here, so I see no reason to force people to change the order they want to apply their decorators in.
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.
OK
# Stub out only the subset of the interface that we actually use in our tests. | ||
class StubClass: | ||
def __init__(self, *args, **kwargs): | ||
super().__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.
I don't believe I have ever seen this in direct object subclass. What does object.__init__()
do?
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.
Presumably nothing, but I believe this is relevant when there is some more complicated MRO.
TBH I can never keep the details straight and I can't remember if I did this because it was necessary or out of habit. Looking at it now, it seems like this is the base of a linear hierarchy, so right now super().__init__()
will always call object.__init__()
, but I'm not sure it's hurting anything...
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 raises questions in a reviewers/readers mind, such as "Did you forget to subclass from somthing else?"
"Verbosity", | ||
] | ||
|
||
from . import strategies |
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.
A linter might recommend removing import as unused. Add comment such # Added to __all__ for users of this module.
?
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.
Shouldn't the linter notice that it's part of __all__
, or that we're in __init__.py
? This is done to expose the strategies
name eagerly, so that import hypothesis; hypothesis.strategies.integers
will work. it's fairly idiomatic, so I don't expect anyone to be surprised by 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.
I was, until I looked up instead of down for its use, and saw usage examples in the tests.
# datetimes is always False. | ||
if py_dt != c_dt: | ||
self.assertEqual( | ||
self._is_ambiguous(py_dt), self._is_ambiguous(c_dt), (py_dt, c_dt), |
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.
Trailing comma is legal but misleading as these are not a series of 'equivalent' args and nothing more can be added.
self._is_ambiguous(py_dt), self._is_ambiguous(c_dt), (py_dt, c_dt), | |
self._is_ambiguous(py_dt), self._is_ambiguous(c_dt), (py_dt, c_dt) |
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.
Hm.. I've been maintaining the style with black
upstream so I think I'd prefer not to change it for trivial reasons, to make merges easier (though this is broken up across multiple lines in the upstream repo, so I dunno what's going on there.
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.
This is not idiomatic in the stdlib I have seen. I understand your concern with your upstream (as in, I won't object further), but this makes me like black even less ;-).
When you're done making the requested changes, leave the comment: |
2 PRs in 1. Do you consider the hypothesis_helper part to be finished and ready to merge?
The content is probably ready to go, but it's still mostly a draft, so it doesn't have niceties like documentation and NEWS and whatnot. I figured I'd get something that works and allow that to be used as a base for implementing something that works.
I agree this is_sort of_ 2 PRs in one, but we don't have anything that uses hypothesis in the standard library, so I think we need something here in order to exercise the code paths. We could switch it out for a simpler set of hypothesis tests (though presumably a simpler set would exercise a smaller subset of the stubs).
If we break it into multiple PRs, we should do it at the end, immediately before merge.
"Verbosity", | ||
] | ||
|
||
from . import strategies |
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.
Shouldn't the linter notice that it's part of __all__
, or that we're in __init__.py
? This is done to expose the strategies
name eagerly, so that import hypothesis; hypothesis.strategies.integers
will work. it's fairly idiomatic, so I don't expect anyone to be surprised by it.
from . import strategies | ||
|
||
|
||
def given(*_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.
The underscores are because it's unused. Might be Google-isms or pylint-isms getting to me, because pylint won't complain about unused arguments if you start them with _.
# If we have found no examples, we must skip the test. If @example | ||
# is applied after @given, it will re-wrap the test to remove the | ||
# skip decorator. |
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.
Hypothesis allows you to do stuff like:
@example(3)
@given(i=strategies.integers())
@example(4)
def test_i(i):
...
It's easy enough to support that here, so I see no reason to force people to change the order they want to apply their decorators in.
# Stub out only the subset of the interface that we actually use in our tests. | ||
class StubClass: | ||
def __init__(self, *args, **kwargs): | ||
super().__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.
Presumably nothing, but I believe this is relevant when there is some more complicated MRO.
TBH I can never keep the details straight and I can't remember if I did this because it was necessary or out of habit. Looking at it now, it seems like this is the base of a linear hierarchy, so right now super().__init__()
will always call object.__init__()
, but I'm not sure it's hurting anything...
# datetimes is always False. | ||
if py_dt != c_dt: | ||
self.assertEqual( | ||
self._is_ambiguous(py_dt), self._is_ambiguous(c_dt), (py_dt, c_dt), |
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.
Hm.. I've been maintaining the style with black
upstream so I think I'd prefer not to change it for trivial reasons, to make merges easier (though this is broken up across multiple lines in the upstream repo, so I dunno what's going on there.
def settings(*args, **kwargs): | ||
pass # pragma: nocover |
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.
@hypothesis.settings(...)
can be used as a decorator, so perhaps
def settings(*args, **kwargs): | |
pass # pragma: nocover | |
def settings(*args, **kwargs): | |
return lambda f: f # pragma: nocover |
return list(cls) | ||
|
||
|
||
class Verbosity(Enum): |
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.
We might also want the Phase
enum?
argstr = ", ".join(self.__stub_args) | ||
kwargstr = ", ".join( | ||
f"{kw}={val}" for kw, val in self.__stub_kwargs.items() | ||
) | ||
|
||
in_parens = argstr | ||
if kwargstr: | ||
in_parens += ", " + kwargstr | ||
|
||
return f"{self.__qualname__}({in_parens})" |
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.
Since the various .map()
, .filter()
, .__or__()
methods don't alter the repr at all, I'd probably prefer that this be less specific - better to represent integers().map(str)
as <stub object at 0x...>
than integers()
!
I also don't think it's worth the code to make the repr track all the methods, unless adding a Stub.__trailing_repr
argument would work?
This PR adds the property tests for
zoneinfo
currently maintained at https://github.com/Zac-HD/stdlib-property-tests, along with a compatibility interface forhypothesis
that gracefully degrades to simple parameterized tests.I haven't stubbed out the entire interface, but I have stubbed out a bit more than I am using as part of the
zoneinfo
tests right now (mostly the subset that I saw in use in the otherstdlib-property-tests
). We can scale down the stubbed interface if we want, and then have people add in the parts they need as it comes up. I don't think it's a good idea to proactively stub out the entirehypothesis
interface, since those things would need maintenance over time.If we like this approach, I will also add in some tooling to run the tests with hypothesis, for use in a buildbot and/or optional PR job.
CC: @Zac-HD, @cfbolz
https://bugs.python.org/issue42109