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

bpo-42109: Implementation of hypothesis stubs for property-based tests, with zoneinfo tests #22863

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Copy link
Member

@pganssle pganssle commented Oct 21, 2020

This PR adds the property tests for zoneinfo currently maintained at https://github.com/Zac-HD/stdlib-property-tests, along with a compatibility interface for hypothesis 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 other stdlib-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 entire hypothesis 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

Copy link
Contributor

@Zac-HD Zac-HD left a comment

Looks great - I'm very excited to get this running 😁

Lib/test/support/_hypothesis_stubs/__init__.py Outdated Show resolved Hide resolved
@github-actions
Copy link

@github-actions github-actions bot commented Dec 16, 2020

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

@github-actions github-actions bot added the stale label Dec 16, 2020
@pganssle pganssle removed the stale label Dec 16, 2020
pganssle added 3 commits May 13, 2021
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():
Copy link
Member Author

@pganssle pganssle May 13, 2021

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.

Copy link
Member

@terryjreedy terryjreedy left a comment

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):
Copy link
Member

@terryjreedy terryjreedy May 25, 2021

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.

Copy link
Member Author

@pganssle pganssle May 25, 2021

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 _.

Copy link
Member

@terryjreedy terryjreedy May 26, 2021

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.
Copy link
Member

@terryjreedy terryjreedy May 25, 2021

Choose a reason for hiding this comment

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

Does hypothesis allow intermixing @given and @examples? Should suggestion for stdlib use be that @given is outermost (topmost) decorator?

Copy link
Member Author

@pganssle pganssle May 25, 2021

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.

Copy link
Member

@terryjreedy terryjreedy May 26, 2021

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__()
Copy link
Member

@terryjreedy terryjreedy May 25, 2021

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?

Copy link
Member Author

@pganssle pganssle May 25, 2021

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...

Copy link
Member

@terryjreedy terryjreedy May 26, 2021

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
Copy link
Member

@terryjreedy terryjreedy May 25, 2021

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.?

Copy link
Member Author

@pganssle pganssle May 25, 2021

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.

Copy link
Member

@terryjreedy terryjreedy May 26, 2021

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),
Copy link
Member

@terryjreedy terryjreedy May 25, 2021

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.

Suggested change
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)

Copy link
Member Author

@pganssle pganssle May 25, 2021

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.

Copy link
Member

@terryjreedy terryjreedy May 26, 2021

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 ;-).

@bedevere-bot
Copy link

@bedevere-bot bedevere-bot commented May 25, 2021

When you're done making the requested changes, leave the comment: I have made the requested changes; please review again.

Copy link
Member Author

@pganssle pganssle left a 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
Copy link
Member Author

@pganssle pganssle May 25, 2021

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):
Copy link
Member Author

@pganssle pganssle May 25, 2021

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.
Copy link
Member Author

@pganssle pganssle May 25, 2021

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__()
Copy link
Member Author

@pganssle pganssle May 25, 2021

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),
Copy link
Member Author

@pganssle pganssle May 25, 2021

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
Copy link
Contributor

@Zac-HD Zac-HD May 26, 2021

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

Suggested change
def settings(*args, **kwargs):
pass # pragma: nocover
def settings(*args, **kwargs):
return lambda f: f # pragma: nocover

return list(cls)


class Verbosity(Enum):
Copy link
Contributor

@Zac-HD Zac-HD May 26, 2021

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})"
Copy link
Contributor

@Zac-HD Zac-HD May 26, 2021

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?

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

Successfully merging this pull request may close these issues.

None yet

5 participants