Repository: HypothesisWorks/hypothesis

Issue:
given prevents function local types from being garbage collected
A class defined inside a function should be suitable to be garbage collected, but given prevents that. Consider the following test:

```python
 def test_local_types_are_garbage_collected():
      store = [None]

      def run_locally():
          class Test(object):
              @given(st.integers())
              def test(self, i):
                  pass
          store[0] = weakref(Test)
          Test().test()
      run_locally()
      del run_locally
      assert store[0] is not None
      gc.collect()
      assert store[0]() is None
```

This test fails, but passes if you remove the given and replace it with a normal method.

I *believe* the likely culprit is the strategy cache - I think the way we're constructing the arguments to the test function is causing something to hang on to a reference to self. This is probably a sign of a more general problem we should worry about if so.


Discussion:
Trawling issues, I suspect that #833 is an example of the (hypothesised) more general problem.  The strategy cache remains the top suspect.

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -65,6 +65,7 @@
 from hypothesis.searchstrategy.strategies import SearchStrategy
 from hypothesis.internal.conjecture.engine import ExitReason, \
     ConjectureRunner, sort_key
+from hypothesis.searchstrategy.collections import TupleStrategy
 
 if False:
     from typing import (  # noqa
@@ -339,12 +340,15 @@ def process_arguments_to_given(
 
     arguments = tuple(arguments)
 
-    search_strategy = st.tuples(
+    # We use TupleStrategy over tuples() here to avoid polluting
+    # st.STRATEGY_CACHE with references (see #493), and because this is
+    # trivial anyway if the fixed_dictionaries strategy is cacheable.
+    search_strategy = TupleStrategy((
         st.just(arguments),
         st.fixed_dictionaries(generator_kwargs).map(
             lambda args: dict(args, **kwargs)
         )
-    )
+    ))
 
     if selfy is not None:
         search_strategy = WithRunner(search_strategy, selfy)


Test patch:
diff --git a/hypothesis-python/tests/nocover/test_cacheable.py b/hypothesis-python/tests/nocover/test_cacheable.py
--- a/hypothesis-python/tests/nocover/test_cacheable.py
+++ b/hypothesis-python/tests/nocover/test_cacheable.py
@@ -17,9 +17,13 @@
 
 from __future__ import division, print_function, absolute_import
 
+import gc
+import weakref
+
 import pytest
 
 import hypothesis.strategies as st
+from hypothesis import given, settings
 
 
 @pytest.mark.parametrize('s', [
@@ -49,3 +53,24 @@ def test_non_cacheable_things_are_not_cached():
 def test_cacheable_things_are_cached():
     x = st.just(())
     assert st.tuples(x) == st.tuples(x)
+
+
+def test_local_types_are_garbage_collected_issue_493():
+    store = [None]
+
+    def run_locally():
+
+        class Test(object):
+            @settings(database=None)
+            @given(st.integers())
+            def test(self, i):
+                pass
+
+        store[0] = weakref.ref(Test)
+        Test().test()
+
+    run_locally()
+    del run_locally
+    assert store[0]() is not None
+    gc.collect()
+    assert store[0]() is None