Repository: HypothesisWorks/hypothesis

Issue:
django_polymorphic breaks model generation
`django_polymorphic` adds mandatory fields (named `*_ptr`) to models, but gives them values when the model is created. Hypothesis sees these as normal non-nullable fields, which trigger the relevant health check. However, explicitly providing a value for one of these fields causes an exception to be thrown in the model's constructor.



Discussion:

Resolving patch:
diff --git a/src/hypothesis/extra/django/models.py b/src/hypothesis/extra/django/models.py
--- a/src/hypothesis/extra/django/models.py
+++ b/src/hypothesis/extra/django/models.py
@@ -23,6 +23,7 @@
 import hypothesis.extra.fakefactory as ff
 from hypothesis.errors import InvalidArgument
 from hypothesis.extra.datetime import datetimes
+from hypothesis.utils.conventions import UniqueIdentifier
 from hypothesis.searchstrategy.strategies import SearchStrategy
 
 
@@ -72,6 +73,9 @@ def add_default_field_mapping(field_type, strategy):
     field_mappings()[field_type] = strategy
 
 
+default_value = UniqueIdentifier(u'default_value')
+
+
 def models(model, **extra):
     result = {}
     mappings = field_mappings()
@@ -96,12 +100,9 @@ def models(model, **extra):
                 u', '.join(missed),
                 model.__name__,
             )))
-    for k, v in extra.items():
-        if isinstance(v, SearchStrategy):
-            result[k] = v
-        else:
-            result[k] = st.just(v)
     result.update(extra)
+    # Remove default_values so we don't try to generate anything for those.
+    result = {k: v for k, v in result.items() if v is not default_value}
     return ModelStrategy(model, result)
 
 


Test patch:
diff --git a/tests/django/toystore/models.py b/tests/django/toystore/models.py
--- a/tests/django/toystore/models.py
+++ b/tests/django/toystore/models.py
@@ -79,3 +79,19 @@ class ManyInts(models.Model):
 
     p1 = models.PositiveIntegerField()
     p2 = models.PositiveSmallIntegerField()
+
+
+class CustomishDefault(models.Model):
+    customish = CustomishField(default=u'b')
+
+
+class MandatoryComputed(models.Model):
+    name = models.CharField(max_length=100, unique=True)
+    company = models.ForeignKey(Company, null=False)
+
+    def __init__(self, **kw):
+        if u'company' in kw:
+            raise RuntimeError()
+        cname = kw[u'name'] + u'_company'
+        kw[u'company'] = Company.objects.create(name=cname)
+        super(MandatoryComputed, self).__init__(**kw)
diff --git a/tests/django/toystore/test_given_models.py b/tests/django/toystore/test_given_models.py
--- a/tests/django/toystore/test_given_models.py
+++ b/tests/django/toystore/test_given_models.py
@@ -21,8 +21,9 @@
 from hypothesis.strategies import just, lists
 from hypothesis.extra.django import TestCase, TransactionTestCase
 from tests.django.toystore.models import Store, Company, Customer, \
-    ManyInts, SelfLoop, Customish, CustomishField, CouldBeCharming
-from hypothesis.extra.django.models import models, \
+    ManyInts, SelfLoop, Customish, CustomishField, CouldBeCharming, \
+    CustomishDefault, MandatoryComputed
+from hypothesis.extra.django.models import models, default_value, \
     add_default_field_mapping
 
 add_default_field_mapping(CustomishField, just(u'a'))
@@ -75,6 +76,25 @@ def test_custom_field(self, x):
     def test_mandatory_fields_are_mandatory(self):
         self.assertRaises(InvalidArgument, models, Store)
 
+    def test_mandatory_computed_fields_are_mandatory(self):
+        self.assertRaises(InvalidArgument, models, MandatoryComputed)
+
+    def test_mandatory_computed_fields_may_not_be_provided(self):
+        mc = models(MandatoryComputed, company=models(Company))
+        self.assertRaises(RuntimeError, mc.example)
+
+    @given(models(MandatoryComputed, company=default_value))
+    def test_mandatory_computed_field_default(self, x):
+        assert x.company.name == x.name + u'_company'
+
+    @given(models(CustomishDefault))
+    def test_customish_default_generated(self, x):
+        assert x.customish == u'a'
+
+    @given(models(CustomishDefault, customish=default_value))
+    def test_customish_default_not_generated(self, x):
+        assert x.customish == u'b'
+
 
 class TestsNeedingRollback(TransactionTestCase):