diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9307d6..9b242448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Add Django 5.0 support ### Changed +- Allow baking without `contenttypes` framework ### Removed - Drop Django 3.2 and 4.1 support (reached end of life) diff --git a/model_bakery/baker.py b/model_bakery/baker.py index 7172ad96..23b23898 100644 --- a/model_bakery/baker.py +++ b/model_bakery/baker.py @@ -17,7 +17,6 @@ from django.apps import apps from django.conf import settings -from django.contrib import contenttypes from django.db.models import ( AutoField, BooleanField, @@ -36,6 +35,7 @@ from . import generators, random_gen from ._types import M, NewM +from .content_types import BAKER_CONTENTTYPES from .exceptions import ( AmbiguousModelName, CustomBakerNotFound, @@ -49,6 +49,13 @@ seq, # noqa: F401 - Enable seq to be imported from recipes ) +if BAKER_CONTENTTYPES: + from django.contrib.contenttypes import models as contenttypes_models + from django.contrib.contenttypes.fields import GenericRelation +else: + contenttypes_models = None + GenericRelation = None + recipes = None # FIXME: use pkg_resource @@ -564,9 +571,7 @@ def is_rel_field(x: str): self.rel_attrs = {k: v for k, v in attrs.items() if is_rel_field(k)} self.rel_fields = [x.split("__")[0] for x in self.rel_attrs if is_rel_field(x)] - def _skip_field(self, field: Field) -> bool: - from django.contrib.contenttypes.fields import GenericRelation - + def _skip_field(self, field: Field) -> bool: # noqa: C901 # check for fill optional argument if isinstance(self.fill_in_optional, bool): field.fill_optional = self.fill_in_optional @@ -588,7 +593,15 @@ def _skip_field(self, field: Field) -> bool: if isinstance(field, OneToOneField) and self._remote_field(field).parent_link: return True - if isinstance(field, (AutoField, GenericRelation, OrderWrt)): + other_fields_to_skip = [ + AutoField, + OrderWrt, + ] + + if BAKER_CONTENTTYPES: + other_fields_to_skip.append(GenericRelation) + + if isinstance(field, tuple(other_fields_to_skip)): return True if all( # noqa: SIM102 @@ -682,9 +695,11 @@ def generate_value(self, field: Field, commit: bool = True) -> Any: # noqa: C90 `attr_mapping` and `type_mapping` can be defined easily overwriting the model. """ - is_content_type_fk = isinstance(field, ForeignKey) and issubclass( - self._remote_field(field).model, contenttypes.models.ContentType - ) + is_content_type_fk = False + if BAKER_CONTENTTYPES: + is_content_type_fk = isinstance(field, ForeignKey) and issubclass( + self._remote_field(field).model, contenttypes_models.ContentType + ) # we only use default unless the field is overwritten in `self.rel_fields` if field.has_default() and field.name not in self.rel_fields: if callable(field.default): @@ -695,7 +710,7 @@ def generate_value(self, field: Field, commit: bool = True) -> Any: # noqa: C90 elif field.choices: generator = random_gen.gen_from_choices(field.choices) elif is_content_type_fk: - generator = self.type_mapping[contenttypes.models.ContentType] + generator = self.type_mapping[contenttypes_models.ContentType] elif generators.get(field.__class__): generator = generators.get(field.__class__) elif field.__class__ in self.type_mapping: diff --git a/model_bakery/content_types.py b/model_bakery/content_types.py new file mode 100644 index 00000000..d0d0c880 --- /dev/null +++ b/model_bakery/content_types.py @@ -0,0 +1,14 @@ +from django.apps import apps + +BAKER_CONTENTTYPES = apps.is_installed("django.contrib.contenttypes") + +default_contenttypes_mapping = {} + +__all__ = ["BAKER_CONTENTTYPES", "default_contenttypes_mapping"] + +if BAKER_CONTENTTYPES: + from django.contrib.contenttypes.models import ContentType + + from . import random_gen + + default_contenttypes_mapping[ContentType] = random_gen.gen_content_type diff --git a/model_bakery/generators.py b/model_bakery/generators.py index 4e89177e..d609be0d 100644 --- a/model_bakery/generators.py +++ b/model_bakery/generators.py @@ -150,14 +150,12 @@ def gen_integer(): def get_type_mapping() -> Dict[Type, Callable]: - from django.contrib.contenttypes.models import ContentType - + from .content_types import default_contenttypes_mapping from .gis import default_gis_mapping mapping = default_mapping.copy() - mapping[ContentType] = random_gen.gen_content_type + mapping.update(default_contenttypes_mapping) mapping.update(default_gis_mapping) - return mapping.copy() diff --git a/tests/conftest.py b/tests/conftest.py index eb0af4f4..f2870662 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,14 +6,18 @@ def pytest_configure(): test_db = os.environ.get("TEST_DB", "sqlite") + use_contenttypes = os.environ.get("USE_CONTENTTYPES", False) installed_apps = [ - "django.contrib.contenttypes", - "django.contrib.auth", "tests.generic", "tests.ambiguous", "tests.ambiguous2", ] + if use_contenttypes: + installed_apps.append("django.contrib.contenttypes") + # auth app depends on contenttypes + installed_apps.append("django.contrib.auth") + using_postgres_flag = False postgis_version = () if test_db == "sqlite": @@ -76,11 +80,11 @@ def pytest_configure(): POSTGIS_VERSION=postgis_version, ) + django.setup() + from model_bakery import baker def gen_same_text(): return "always the same text" baker.generators.add("tests.generic.fields.CustomFieldViaSettings", gen_same_text) - - django.setup() diff --git a/tests/generic/models.py b/tests/generic/models.py index 6bd6ba59..789c7e7a 100755 --- a/tests/generic/models.py +++ b/tests/generic/models.py @@ -8,11 +8,10 @@ import django from django.conf import settings -from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation -from django.contrib.contenttypes.models import ContentType from django.core.files.storage import FileSystemStorage from django.utils.timezone import now +from model_bakery.baker import BAKER_CONTENTTYPES from model_bakery.gis import BAKER_GIS from model_bakery.timezone import tz_aware @@ -37,6 +36,16 @@ else: from django.db import models + +# check if the contenttypes app is installed +if BAKER_CONTENTTYPES: + from django.contrib.contenttypes import models as contenttypes + from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +else: + contenttypes = None + GenericRelation = None + GenericForeignKey = None + GENDER_CHOICES = [ ("M", "male"), ("F", "female"), @@ -272,14 +281,17 @@ class UnsupportedModel(models.Model): unsupported_field = UnsupportedField() -class DummyGenericForeignKeyModel(models.Model): - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey("content_type", "object_id") +if BAKER_CONTENTTYPES: + class DummyGenericForeignKeyModel(models.Model): + content_type = models.ForeignKey( + contenttypes.ContentType, on_delete=models.CASCADE + ) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey("content_type", "object_id") -class DummyGenericRelationModel(models.Model): - relation = GenericRelation(DummyGenericForeignKeyModel) + class DummyGenericRelationModel(models.Model): + relation = GenericRelation(DummyGenericForeignKeyModel) class DummyNullFieldsModel(models.Model): diff --git a/tests/test_baker.py b/tests/test_baker.py index 4f03d411..e3bebde3 100644 --- a/tests/test_baker.py +++ b/tests/test_baker.py @@ -3,8 +3,8 @@ from decimal import Decimal from unittest.mock import patch +from django.apps import apps from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.db.models import Manager from django.db.models.signals import m2m_changed from django.test import TestCase, override_settings @@ -12,7 +12,7 @@ import pytest from model_bakery import baker, random_gen -from model_bakery.baker import MAX_MANY_QUANTITY +from model_bakery.baker import BAKER_CONTENTTYPES, MAX_MANY_QUANTITY from model_bakery.exceptions import ( AmbiguousModelName, InvalidQuantityException, @@ -232,6 +232,11 @@ def test_accepts_generators_with_quantity_for_unique_fields(self): assert num_2.value == 2 assert num_3.value == 3 + # skip if auth app is not installed + @pytest.mark.skipif( + not apps.is_installed("django.contrib.auth"), + reason="Django auth app is not installed", + ) def test_generators_work_with_user_model(self): from django.contrib.auth import get_user_model @@ -602,6 +607,9 @@ def test_unsupported_model_raises_an_explanatory_exception(self): assert "field unsupported_field" in repr(e) +@pytest.mark.skipif( + not BAKER_CONTENTTYPES, reason="Django contenttypes framework is not installed" +) @pytest.mark.django_db class TestHandlingModelsWithGenericRelationFields: def test_create_model_with_generic_relation(self): @@ -609,16 +617,26 @@ def test_create_model_with_generic_relation(self): assert isinstance(dummy, models.DummyGenericRelationModel) +@pytest.mark.skipif( + not BAKER_CONTENTTYPES, reason="Django contenttypes framework is not installed" +) @pytest.mark.django_db class TestHandlingContentTypeField: def test_create_model_with_contenttype_field(self): + from django.contrib.contenttypes.models import ContentType + dummy = baker.make(models.DummyGenericForeignKeyModel) assert isinstance(dummy, models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) +@pytest.mark.skipif( + not BAKER_CONTENTTYPES, reason="Django contenttypes framework is not installed" +) class TestHandlingContentTypeFieldNoQueries: def test_create_model_with_contenttype_field(self): + from django.contrib.contenttypes.models import ContentType + # Clear ContentType's internal cache so that it *will* try to connect to # the database to fetch the corresponding ContentType model for # a randomly chosen model. diff --git a/tests/test_filling_fields.py b/tests/test_filling_fields.py index c52f43b4..6f25f638 100644 --- a/tests/test_filling_fields.py +++ b/tests/test_filling_fields.py @@ -5,7 +5,6 @@ from tempfile import gettempdir from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.core.validators import ( validate_ipv4_address, validate_ipv6_address, @@ -17,6 +16,7 @@ import pytest from model_bakery import baker +from model_bakery.content_types import BAKER_CONTENTTYPES from model_bakery.gis import BAKER_GIS from model_bakery.random_gen import gen_related from tests.generic import generators, models @@ -271,9 +271,15 @@ def test_filling_IPAddressField(self): validate_ipv46_address(obj.ipv46_field) +# skipif +@pytest.mark.skipif( + not BAKER_CONTENTTYPES, reason="Django contenttypes framework is not installed" +) @pytest.mark.django_db class TestFillingGenericForeignKeyField: def test_filling_content_type_field(self): + from django.contrib.contenttypes.models import ContentType + dummy = baker.make(models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) assert dummy.content_type.model_class() is not None @@ -285,6 +291,8 @@ def test_iteratively_filling_generic_foreign_key_field(self): Otherwise, calling ``next()`` when a GFK is in ``iterator_attrs`` would be bypassed. """ + from django.contrib.contenttypes.models import ContentType + objects = baker.make(models.Profile, _quantity=2) dummies = baker.make( models.DummyGenericForeignKeyModel, @@ -579,7 +587,6 @@ def assertGeomValid(self, geom): assert geom.valid is True, geom.valid_reason def test_fill_PointField_valid(self, person): - print(BAKER_GIS) self.assertGeomValid(person.point) def test_fill_LineStringField_valid(self, person): diff --git a/tox.ini b/tox.ini index c77ea10c..2f35e4ec 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ env_list = py{38,39}-django{42}-{postgresql,sqlite} py{310,311}-django{42,50}-{postgresql,sqlite} py{311,312}-django{42,50}-{postgresql-psycopg3} + py312-django50-{postgresql-contenttypes} [testenv] package = wheel @@ -14,6 +15,7 @@ setenv = postgresql-psycopg3: TEST_DB=postgis postgresql-psycopg3: PGUSER=postgres postgresql-psycopg3: PGPASSWORD=postgres + postgresql-contenttypes: USE_CONTENTTYPES=True sqlite: TEST_DB=sqlite sqlite: USE_TZ=True deps =