Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #265 -- drop hard dependency on contenttypes framework #476

Merged
merged 1 commit into from Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
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
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
@@ -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
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
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
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
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
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
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