Skip to content

Commit

Permalink
Fix #265 -- drop hard dependency on contenttypes framework (#476)
Browse files Browse the repository at this point in the history
  • Loading branch information
amureki authored Apr 21, 2024
1 parent 1d75853 commit aa2955f
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 24 additions & 9 deletions model_bakery/baker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions model_bakery/content_types.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 2 additions & 4 deletions model_bakery/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
12 changes: 8 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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()
28 changes: 20 additions & 8 deletions tests/generic/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"),
Expand Down Expand Up @@ -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):
Expand Down
22 changes: 20 additions & 2 deletions tests/test_baker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
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

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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -602,23 +607,36 @@ 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):
dummy = baker.make(models.DummyGenericRelationModel)
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.
Expand Down
11 changes: 9 additions & 2 deletions tests/test_filling_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down

0 comments on commit aa2955f

Please sign in to comment.