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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints to the public API, with generic Strategy type #1253

Merged
merged 4 commits into from May 9, 2018

Conversation

Zac-HD
Copy link
Member

@Zac-HD Zac-HD commented Apr 24, 2018

Does not quite close #200 (next time though! 馃帀)

I won't recite the whole motivation here - suffice to say that at least some people like static typing, and so I'd like to let the type-checker find some of their obvious misuses of Hypothesis.

Things not obvious from review:

  • Mypy doesn't support arity-specific overloads, which would be useful for tuples.
  • The disallow_untyped_decorators option is indispensable if you don't want a a decorator to erase the type hints on a function.

I would also like to write some tests using reveal_type to check that Mypy does in fact infer the expected type for various expressions. This may go in a later PR, to keep things reviewable - I've confirmed most of this manually, so automated tests would really be for tooling or code regressions.

@Zac-HD Zac-HD force-pushed the typed branch 5 times, most recently from 115f439 to e7be891 Compare April 25, 2018 13:23
@Zac-HD
Copy link
Member Author

Zac-HD commented Apr 25, 2018

check-shellcheck was fixed in #1255, so the build is passing now 馃槃

Ready for review!

@Zac-HD
Copy link
Member Author

Zac-HD commented May 2, 2018

Ping @HypothesisWorks/hypothesis-python-contributors for review!

@kxepal
Copy link
Member

kxepal commented May 2, 2018

LGFM.

@Zac-HD
I'm sorry for being late for the mypy party, but do you have a references with explanation or why did you pick comment-based typing instead of stubs (just curious, though I think I understand why) and what is the if False hack origins from? That one looks pretty weird for the first sight.

@Zac-HD
Copy link
Member Author

Zac-HD commented May 2, 2018

Do you have a references with explanation or why did you pick comment-based typing instead of stubs (just curious, though I think I understand why)

I picked comments over stubs for a couple of reasons.

  1. Distribution is harder to screw up. Stub files introduce a whole new wrinkle for downstream packagers to deal with, while they're already distributing Hypothesis with comments! Writing the build rule to include a single empty file (py.typed) is much easier for us too.
  2. Locality of reference. It's much more difficult to edit a function signature but forget the type hints when they're on adjacent lines, and easier to catch in review if it happens anyway. Hopefully this will make the transition easier if or when we ditch Python 2 and use the inline annotation syntax.
  3. IMO it's easier to extend incrementally to a few functions or even arguments at a time if you're using comments.

What is the if False hack origins from? That one looks pretty weird for the first sight.

There's a constant typing.TYPE_CHECKING (ie False at runtime) which is designed to guard imports. Luckily Mypy also accepts a literal False, saving us some silly juggling of imports wherever we need to import a type. There's no deep logic to it, just the Mypy devs recognising that this was a useful pattern.

@kxepal
Copy link
Member

kxepal commented May 2, 2018

Thank you!

@Zac-HD
Copy link
Member Author

Zac-HD commented May 8, 2018

Ping @DRMacIver for review - it's been almost two weeks!

I'd really like to get this merged soon so I can focus on PyCon preparation 馃槃


except ImportError: # pragma: no cover
Ex = 'key' # type: ignore
Generic = {Ex: object} # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

I actual cracked up at this hack.

(It's a perfectly reasonable solution but for some reason struck me as very funny)

pass # pragma: no cover


@overload # noqa: F811 # redefinition of sampled_from
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should just turn off this warning in config? I'm not sure it gives us much of use and these noqas are annoying.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe my tendency to never-weaken-default-settings is going too far here, yes.

up our type hints.

.. note::
Hypothesis' type hints may make breaking changes between minor releases.
Copy link
Member

Choose a reason for hiding this comment

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

Should the paragraphs below be part of this note? They qualify it, so I think it makes sense to have them logically grouped.

It also might be nice to make clear that we do plan to stabilise it eventually (but not before the main typing module becomes non-provisional)

Copy link
Member Author

Choose a reason for hiding this comment

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

Expanding note, yes. IMO the plans to stabilise are implicit, and I'd rather leave them that way until we have some more experience and understanding of how often they break over time.


.. doctest::

>>> from hypothesis.strategies import SearchStrategy
Copy link
Member

Choose a reason for hiding this comment

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

Does this doctest really add much? It seems weird in context as it's only asserting something about the dynamic subtyping behaviour in a section where we talk about how the only valid use case of this type is static type checking.

Copy link
Member Author

Choose a reason for hiding this comment

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

...I'll take it out, you're right.

History: this is how I found the problem with unannotated decorators erasing type hints. More on that below...

- ``integers()`` is of type ``SearchStrategy[int]``.
- ``lists(integers())`` is of type ``SearchStrategy[List[int]]``.
- ``SearchStrategy[bool]`` is a subtype of ``SearchStrategy[int]``,
because ``bool`` is a subtype of ``int``.
Copy link
Member

Choose a reason for hiding this comment

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

nit: bool being a subtype of int is weird enough that I'd probably prefer a different example. Maybe just that it's a subtype of SearchStrategy[Any]?

Please do not inherit from, compare to, or otherwise use the
:class:`~hypothesis.strategies.SearchStrategy` in any way outside of type
hints. This is a private, internal interface that is *only* available for
type checking, and may be changed or removed in any minor release.
Copy link
Member

Choose a reason for hiding this comment

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

I think that's a bit strong. We can probably commit to keeping the name around without a major release bump.

def given(*given_arguments, **given_kwargs):
# type: (*SearchStrategy, **SearchStrategy) -> Callable
def given(
*given_arguments, # type: Union[SearchStrategy, UniqueIdentifier]
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't need to be as part of this PR, but it would be nice to tighten up these types to be more specific about which UniqueIdentifier is permitted here, if only so the UniqueIdentifier type doesn't appear in our public APIs.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll include that too; it's pretty easy to toss in class InferType(UniqueIdentifier): ..., infer = InferType('infer') (better name welcome), and edit the hints.

@Zac-HD
Copy link
Member Author

Zac-HD commented May 8, 2018

Note on release plans: as this stands, there are no automated tests that will catch regressions in the type hints as seen by downstream users. With some distance from the initial implementation, that makes me uncomfortable - I'm pretty sure that means that they'll break soon, or simply aren't working at all yet.

In the interest of reviewable diffs, I therefore propose to replace the docs+release commit with a smaller one that does another non-user-facing patch, and then do a follow-up PR later this week with tests and the public release of type annotations. Sound good?

@Zac-HD Zac-HD force-pushed the typed branch 4 times, most recently from 5c021f3 to f03a034 Compare May 8, 2018 13:03
@Zac-HD
Copy link
Member Author

Zac-HD commented May 8, 2018

Updated for another @DRMacIver review 馃槃

@@ -27,5 +27,14 @@ def __repr__(self):
return self.identifier


infer = UniqueIdentifier(u'infer')
class DefaultValueType(UniqueIdentifier):
# Using subclasses so they can be distinguished by e.g. Mypy
Copy link
Member

Choose a reason for hiding this comment

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

nit: This comment should probably go at the top level as it applies to two classes, not just this one.

(so Mypy can tell you're using the wrong one)
@Zac-HD Zac-HD merged commit 661fd78 into HypothesisWorks:master May 9, 2018
@Zac-HD Zac-HD deleted the typed branch May 9, 2018 00:35
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.

Add static typing to Hypothesis API
3 participants