Repository: HypothesisWorks/hypothesis

Issue:
Problems with hypothesis entrypoints
Currently I am testing out this new cool `entrypoints` feature. #2556 
And it does not work correctly for me.
I specify an entrypoint and nothing happens. The entrypoint function is not getting called.

I am following this example: https://github.com/HypothesisWorks/hypothesis/commit/4ca04cf85b1389f0233dc652a5a74f15d2922c05#diff-94fd9cebb412c332ed91ff584ee7c2f3R129-R155

The problem is best shown by this piece of code:

```python
# module.py

print('Module imported!')

def _hypothesis_setup_hook():
    print('func called')
```

Then I add it to the `setup.py` the same way it is shown in the docs.
Now, let's try to call `run()` function:

```python
>>> for entry_point in pkg_resources.iter_entry_points("hypothesis"):
...     print(entry_point)
...     entry_point.load()
```

Output:

```
_ = mymodule:_hypothesis_setup_hook
Module imported!
<function _hypothesis_setup_hook at 0x100f20ca0>
```

When I change this code to actually execute a function, it works:

```python
>>> for entry_point in pkg_resources.iter_entry_points("hypothesis"):
...     entry_point.load()()  # calling it now
```

Outputs:

```
Module imported!
func called
```
Avoid importing pkg_resources
Hypothesis recently started importing `pkg_resources` which is [notoriously slow](https://github.com/pypa/setuptools/issues/510) to import. A better alternative these days is to use [`importlib.metadata`](https://docs.python.org/3/library/importlib.metadata.html), added in Python 3.8, or its backport `importlib_metadata`. Doing so would look something like this (completely untested, just to get the idea across):

```diff
diff --git a/hypothesis-python/src/hypothesis/entry_points.py b/hypothesis-python/src/hypothesis/entry_points.py
index 8cc4137ba..a5d670143 100644
--- a/hypothesis-python/src/hypothesis/entry_points.py
+++ b/hypothesis-python/src/hypothesis/entry_points.py
@@ -20,9 +20,18 @@ custom types, running the relevant code when *hypothesis* is imported instead of
 your package.
 """
 
-import pkg_resources
+try:
+    if sys.version_info >= (3, 8):
+        from importlib import metadata as importlib_metadata
+    else:
+        import importlib_metadata
+except ImportError:
+    import pkg_resources
 
-
-def run():
-    for entry_point in pkg_resources.iter_entry_points("hypothesis"):
-        entry_point.load()  # pragma: no cover
+    def run():
+        for entry_point in pkg_resources.iter_entry_points("hypothesis"):
+            entry_point.load()  # pragma: no cover
+else:
+    def run():
+        for entry_point in importlib_metadata.entry_points().get("hypothesis", []):
+            entry_point.load()  # pragma: no cover
```




Discussion:
Whoops.  I'll want to keep it working for modules, but add `if callable(hook): hook()` to handle the function case.


Resolving patch:
diff --git a/hypothesis-python/examples/example_hypothesis_entrypoint/example_hypothesis_entrypoint.py b/hypothesis-python/examples/example_hypothesis_entrypoint/example_hypothesis_entrypoint.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/examples/example_hypothesis_entrypoint/example_hypothesis_entrypoint.py
@@ -0,0 +1,32 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Most of this work is copyright (C) 2013-2020 David R. MacIver
+# (david@drmaciver.com), but it contains contributions by others. See
+# CONTRIBUTING.rst for a full list of people who may hold copyright, and
+# consult the git log if you need to determine who owns an individual
+# contribution.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+#
+# END HEADER
+
+"""This example demonstrates a setuptools entry point.
+
+See https://hypothesis.readthedocs.io/en/latest/strategies.html#registering-strategies-via-setuptools-entry-points
+for details and documentation.
+"""
+
+
+class MyCustomType:
+    def __init__(self, x: int):
+        assert x >= 0, f"got {x}, but only positive numbers are allowed"
+        self.x = x
+
+
+def _hypothesis_setup_hook():
+    import hypothesis.strategies as st
+
+    st.register_type_strategy(MyCustomType, st.integers(min_value=0).map(MyCustomType))
diff --git a/hypothesis-python/examples/example_hypothesis_entrypoint/setup.py b/hypothesis-python/examples/example_hypothesis_entrypoint/setup.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/examples/example_hypothesis_entrypoint/setup.py
@@ -0,0 +1,32 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Most of this work is copyright (C) 2013-2020 David R. MacIver
+# (david@drmaciver.com), but it contains contributions by others. See
+# CONTRIBUTING.rst for a full list of people who may hold copyright, and
+# consult the git log if you need to determine who owns an individual
+# contribution.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+#
+# END HEADER
+
+"""Minimal setup.py to register an entrypoint."""
+
+import setuptools
+
+setuptools.setup(
+    name="example_hypothesis_entrypoint",
+    author="Example Author",
+    email="author@example.com",
+    license="MPL v2",
+    description="Minimal setup.py to register an entrypoint.",
+    packages=setuptools.find_packages(),
+    install_requires=["hypothesis"],
+    python_requires=">=3.6",
+    entry_points={
+        "hypothesis": ["_ = example_hypothesis_entrypoint:_hypothesis_setup_hook"]
+    },
+)
diff --git a/hypothesis-python/src/hypothesis/entry_points.py b/hypothesis-python/src/hypothesis/entry_points.py
--- a/hypothesis-python/src/hypothesis/entry_points.py
+++ b/hypothesis-python/src/hypothesis/entry_points.py
@@ -20,9 +20,29 @@
 your package.
 """
 
-import pkg_resources
+try:
+    # We prefer to use importlib.metadata, or the backport on Python <= 3.7,
+    # because it's much faster than pkg_resources (200ms import time speedup).
+    try:
+        from importlib import metadata as importlib_metadata
+    except ImportError:
+        import importlib_metadata  # type: ignore  # mypy thinks this is a redefinition
+
+    def get_entry_points():
+        yield from importlib_metadata.entry_points().get("hypothesis", [])
+
+
+except ImportError:
+    # But if we're not on Python >= 3.8 and the importlib_metadata backport
+    # is not installed, we fall back to pkg_resources anyway.
+    import pkg_resources
+
+    def get_entry_points():
+        yield from pkg_resources.iter_entry_points("hypothesis")
 
 
 def run():
-    for entry_point in pkg_resources.iter_entry_points("hypothesis"):
-        entry_point.load()  # pragma: no cover
+    for entry in get_entry_points():  # pragma: no cover
+        hook = entry.load()
+        if callable(hook):
+            hook()


Test patch:
diff --git a/hypothesis-python/examples/example_hypothesis_entrypoint/test_entrypoint.py b/hypothesis-python/examples/example_hypothesis_entrypoint/test_entrypoint.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/examples/example_hypothesis_entrypoint/test_entrypoint.py
@@ -0,0 +1,26 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Most of this work is copyright (C) 2013-2020 David R. MacIver
+# (david@drmaciver.com), but it contains contributions by others. See
+# CONTRIBUTING.rst for a full list of people who may hold copyright, and
+# consult the git log if you need to determine who owns an individual
+# contribution.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+#
+# END HEADER
+
+from example_hypothesis_entrypoint import MyCustomType
+
+from hypothesis import given, strategies as st
+
+
+@given(st.from_type(MyCustomType))
+def test_registered_from_entrypoint(x):
+    # This demonstrates that we've registered the type, not just
+    # worked out how to construct an instance.
+    assert isinstance(x, MyCustomType)
+    assert x.x >= 0