Skip to content

Add _full_clean flag to baker.make(), prepare(), and bulk_create()#591

Open
benaduo wants to merge 1 commit intomodel-bakers:mainfrom
benaduo:fix/full-clean-flag
Open

Add _full_clean flag to baker.make(), prepare(), and bulk_create()#591
benaduo wants to merge 1 commit intomodel-bakers:mainfrom
benaduo:fix/full-clean-flag

Conversation

@benaduo
Copy link
Copy Markdown
Contributor

@benaduo benaduo commented Mar 27, 2026

Fixes #523

Adds _full_clean=False to baker.make(), baker.prepare(), and baker.bulk_create(). When True, calls Django's full_clean() on each instance before saving, enabling model validation during test object creation.

For _bulk_create=True, validation runs at prepare() time and the bulk_create call is wrapped in transaction.atomic() to ensure atomic rollback if any validation fails.

Fresh implementation based on the approach in #547 (thanks @berinhard), incorporating feedback from Rust's review of that PR.

Changes

  • model_bakery/baker.py_full_clean param threaded through all make()/prepare() overloads and implementations down to Baker.instance(); bulk_create() wrapped in transaction.atomic() when flag is set
  • tests/test_baker.py — new TestFullClean class with 5 tests covering valid creation, invalid raises, no DB entry on error, prepare path, and bulk_create rollback
  • docs/basic_usage.md — new "Running Model Validation" section
  • CHANGELOG.md — entries under Added and Changed

Summary by CodeRabbit

  • New Features

    • Optional model validation during object creation and preparation (disabled by default via a new flag).
    • When validation is enabled for bulk creation, the entire bulk operation runs inside a transaction so validation failures roll back all changes.
  • Documentation

    • Added guidance and examples showing how to enable validation and expected error behavior for single and bulk creation.
  • Tests

    • Added tests covering validation success/failure, error raising, and bulk-create rollback semantics.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an optional _full_clean: bool = False parameter to make(), prepare(), and bulk_create() so Django model validation (instance.full_clean()) can be run during creation. When _full_clean=True, validation errors raise ValidationError; when combined with _bulk_create=True, bulk creation and related persistence are executed inside a transaction so failures roll back atomically.

Changes

Cohort / File(s) Summary
Core Implementation
model_bakery/baker.py
Added _full_clean: bool = False to public APIs and Baker internals; call instance.full_clean() during instance construction when requested; propagate the flag through quantity and bulk flows; when _full_clean is truthy, wrap related persistence plus manager.bulk_create(entries) in transaction.atomic(using=...).
Tests
tests/test_baker.py
Added TestFullClean cases and imported ValidationError: verify successful save for valid data with _full_clean=True, assert ValidationError and no DB rows for invalid inputs, and assert atomic rollback for _bulk_create=True + _full_clean=True.
Documentation
docs/basic_usage.md
Added "Running Model Validation" section documenting _full_clean=True usage for baker.make()/baker.prepare() and explaining atomic rollback when combined with _bulk_create=True.
Changelog
CHANGELOG.md
Documented new _full_clean parameter for make()/prepare()/bulk_create() and the conditional transaction wrapping for bulk-create rollback on validation errors.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Caller
  participant Baker as Baker
  participant Model as DjangoModel
  participant DB as Database

  Caller->>Baker: baker.make(..., _full_clean=True)
  Baker->>Model: construct instance (apply fields, relations)
  Baker->>Model: instance.full_clean()
  alt validation passes
    Baker->>DB: begin transaction (if _bulk_create)
    Baker->>Model: instance.save()
    Baker->>DB: commit (if _bulk_create)
    Baker-->>Caller: return saved instance(s)
  else validation fails
    Baker->>DB: rollback (if _bulk_create)
    Baker-->>Caller: raise ValidationError
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through fields both wide and keen,
Full clean in paw to keep each record clean,
Transactions cozy, rollback on wrong,
Baker and I hum a tidy song,
Data safe — I nibble on a carrot, pleased.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a _full_clean flag to three key baker functions.
Linked Issues check ✅ Passed The PR fully implements the feature requested in issue #523 by adding _full_clean parameter to baker.make(), prepare(), and bulk_create().
Out of Scope Changes check ✅ Passed All changes are directly scoped to the requested feature: adding _full_clean flag support and related validation/transaction handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
model_bakery/baker.py (2)

464-518: ⚠️ Potential issue | 🟠 Major

Propagate _full_clean into autogenerated FK/O2O objects.

_full_clean only reaches self.instance(). The relational path still goes through generate_value(), and that code only forwards _create_files for relation generators. So baker.make(models.User, profile__email="not-an-email", _full_clean=True) will still save an invalid related Profile, even though validation was explicitly requested.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_bakery/baker.py` around lines 464 - 518, The _full_clean flag is only
passed to self.instance() but not into related-object creation: update calls to
self.generate_value(field, commit_related) in bake/create paths to include and
forward the _full_clean parameter (e.g., self.generate_value(field,
commit_related, _full_clean=_full_clean)), update the generate_value(...)
signature to accept _full_clean and ensure any code paths that
create/autogenerate related OneToOne/ForeignKey objects forward that flag into
their calls to instance(...) (and into any relation generators) so related
models are validated when _full_clean=True.

950-979: ⚠️ Potential issue | 🟠 Major

Use an alias-bound atomic block around the entire bulk-write phase.

When baker._using specifies a non-default database, transaction.atomic() (without the using parameter) applies only to the default database, not to baker._using. The manager.bulk_create() call operates on baker._using but runs outside any transaction on that database when _full_clean=False, and in a transaction bound to the default database when _full_clean=True—leaving related objects inserted by _save_related_objs() (which runs outside the atomic block) unprotected. Use transaction.atomic(using=baker._using) and wrap the entire bulk-write phase including _save_related_objs() to ensure consistency across multi-database setups.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_bakery/baker.py` around lines 950 - 979, The bulk_create function
currently calls _save_related_objs and manager.bulk_create outside a
database-scoped atomic when baker._using targets a non-default DB; wrap the
entire bulk-write phase (the call to _save_related_objs and the
manager.bulk_create call) in a transaction.atomic using the proper database
alias: use transaction.atomic(using=baker._using) when baker._using is truthy,
otherwise use a default transaction.atomic(), so the related-object inserts and
the bulk_create are executed in the same atomic block tied to the correct DB.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Around line 12-16: Update the changelog wording to reference the user-facing
call shape using the _bulk_create flag (e.g., baker.make(..., _bulk_create=True)
and baker.prepare(..., _bulk_create=True)) instead of pointing readers to the
internal baker.bulk_create(model, ...) helper; reword the bullet to mention the
_bulk_create=True API and keep the note about atomic transaction behavior when
_full_clean=True validation fails.

---

Outside diff comments:
In `@model_bakery/baker.py`:
- Around line 464-518: The _full_clean flag is only passed to self.instance()
but not into related-object creation: update calls to self.generate_value(field,
commit_related) in bake/create paths to include and forward the _full_clean
parameter (e.g., self.generate_value(field, commit_related,
_full_clean=_full_clean)), update the generate_value(...) signature to accept
_full_clean and ensure any code paths that create/autogenerate related
OneToOne/ForeignKey objects forward that flag into their calls to instance(...)
(and into any relation generators) so related models are validated when
_full_clean=True.
- Around line 950-979: The bulk_create function currently calls
_save_related_objs and manager.bulk_create outside a database-scoped atomic when
baker._using targets a non-default DB; wrap the entire bulk-write phase (the
call to _save_related_objs and the manager.bulk_create call) in a
transaction.atomic using the proper database alias: use
transaction.atomic(using=baker._using) when baker._using is truthy, otherwise
use a default transaction.atomic(), so the related-object inserts and the
bulk_create are executed in the same atomic block tied to the correct DB.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ef3653d-f67b-4f4f-88d3-8dad01a7be62

📥 Commits

Reviewing files that changed from the base of the PR and between b094155 and 87f088c.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • docs/basic_usage.md
  • model_bakery/baker.py
  • tests/test_baker.py

CHANGELOG.md Outdated
Comment on lines +12 to +16
- Add `_full_clean` flag to `baker.make()`, `baker.prepare()`, and `baker.bulk_create()` to run Django model validation (`False` by default) ([#523](https://github.com/model-bakers/model_bakery/issues/523))

### Changed

- Bulk creation now uses transaction blocks to ensure atomic rollback when `_full_clean=True` validation fails
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the _bulk_create=True call shape here.

The user-facing API is baker.make(..., _bulk_create=True), not baker.bulk_create(model, ...). As written, this changelog entry points readers at an internal helper with a different signature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 12 - 16, Update the changelog wording to reference
the user-facing call shape using the _bulk_create flag (e.g., baker.make(...,
_bulk_create=True) and baker.prepare(..., _bulk_create=True)) instead of
pointing readers to the internal baker.bulk_create(model, ...) helper; reword
the bullet to mention the _bulk_create=True API and keep the note about atomic
transaction behavior when _full_clean=True validation fails.

@benaduo benaduo force-pushed the fix/full-clean-flag branch 3 times, most recently from 551811b to c6e5c36 Compare March 27, 2026 19:11
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_bakery/baker.py`:
- Around line 151-157: The bug is that _save_kwargs is being forwarded into the
prepare/bulk-prepare path where Baker.prepare() doesn't expect it, causing it to
be treated as a model attribute and breaking _bulk_create; fix by stripping
_save_kwargs (and similarly _full_clean if intended only for save) out of the
kwargs before calling baker.prepare() or bulk_create() so only real model attrs
are forwarded. Concretely, in the caller that invokes
baker.prepare()/bulk_create() (the code that calls bulk_create(baker, ...) and
the code path that forwards kwargs into Baker.prepare()), remove any key named
"_save_kwargs" from the dict passed to prepare()/bulk-prepare (e.g.,
pop("_save_kwargs", None)) and ensure _save_kwargs is only consumed by the
save/bulk create code paths that actually use it (such as _bulk_create or the
save logic).
- Around line 986-990: The current atomic block uses the default DB connection
and only wraps manager.bulk_create when _full_clean is true, leaving
_save_related_objs and writes on non-default DBs unprotected; change the flow so
that both _save_related_objs(...) and manager.bulk_create(entries) are executed
inside a single transaction.atomic(using=baker._using) context (regardless of
_full_clean) so that writes use the correct database alias and related FK rows
are rolled back together on failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2dca0793-5be8-432f-9068-30650f4a7188

📥 Commits

Reviewing files that changed from the base of the PR and between d3f15ee and 551811b.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • docs/basic_usage.md
  • model_bakery/baker.py
  • tests/test_baker.py
✅ Files skipped from review due to trivial changes (2)
  • docs/basic_usage.md
  • tests/test_baker.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment on lines +151 to +157
result = bulk_create(
baker,
_quantity or 1,
_save_kwargs=_save_kwargs,
_full_clean=_full_clean,
**attrs,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't send _save_kwargs down the bulk-prepare path.

Line 154 always passes _save_kwargs, and Lines 971-974 forward kwargs unchanged into baker.prepare(). Baker.prepare() doesn't consume _save_kwargs, so it falls through into model attrs and breaks _bulk_create before full_clean() or bulk_create() run.

Minimal fix
-        result = bulk_create(
-            baker,
-            _quantity or 1,
-            _save_kwargs=_save_kwargs,
-            _full_clean=_full_clean,
-            **attrs,
-        )
+        result = bulk_create(
+            baker,
+            _quantity or 1,
+            _full_clean=_full_clean,
+            **attrs,
+        )

Also applies to: 959-974

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_bakery/baker.py` around lines 151 - 157, The bug is that _save_kwargs
is being forwarded into the prepare/bulk-prepare path where Baker.prepare()
doesn't expect it, causing it to be treated as a model attribute and breaking
_bulk_create; fix by stripping _save_kwargs (and similarly _full_clean if
intended only for save) out of the kwargs before calling baker.prepare() or
bulk_create() so only real model attrs are forwarded. Concretely, in the caller
that invokes baker.prepare()/bulk_create() (the code that calls
bulk_create(baker, ...) and the code path that forwards kwargs into
Baker.prepare()), remove any key named "_save_kwargs" from the dict passed to
prepare()/bulk-prepare (e.g., pop("_save_kwargs", None)) and ensure _save_kwargs
is only consumed by the save/bulk create code paths that actually use it (such
as _bulk_create or the save logic).

Comment on lines +986 to +990
if _full_clean:
with transaction.atomic():
created_entries = manager.bulk_create(entries)
else:
created_entries = manager.bulk_create(entries)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Django, which database connection does transaction.atomic()use whenusingis omitted, and how should it be paired withManager.using(...) in multi-database code?

💡 Result:

When using is omitted in transaction.atomic, it uses the "default" database connection. This is explicitly stated in the official Django documentation across multiple versions. In multi-database code, transaction.atomic only manages transactions on the specified (or default) database connection—it does not affect other databases. Queries executed via Manager.using('alias') (e.g., Model.objects.using('other_db').create(...)) will use the specified database alias regardless of the surrounding atomic block. The transaction management for those queries depends on whether an atomic block using that same alias is active. To ensure atomicity across operations on a specific non-default database: - Use with transaction.atomic(using='alias'): - Then perform queries with Model.objects.using('alias')... Example: from django.db import transaction with transaction.atomic(using='other_db'): obj1 = MyModel.objects.using('other_db').create(...) obj2 = MyModel.objects.using('other_db').update(...) This opens a transaction on 'other_db', executes the queries there, and commits/rolls back based on success. Note: True distributed (2-phase commit) transactions across multiple databases are not supported by Django; each database has independent transactions. Use separate nested atomic(using=...) blocks for operations spanning databases, but be aware failures in one won't automatically rollback others.

Citations:


🏁 Script executed:

cat -n model_bakery/baker.py | sed -n '970,995p'

Repository: model-bakers/model_bakery

Length of output: 1007


🏁 Script executed:

# Check if manager.db is used elsewhere in the codebase
rg "manager\.db" model_bakery/

# Also check what attributes are available on Django managers for database selection
rg "\.using\(" model_bakery/baker.py | head -20

Repository: model-bakers/model_bakery

Length of output: 132


🏁 Script executed:

# Search for Django manager database attributes in documentation or code
rg "\.db\b" model_bakery/ | head -10

# Check what variables are available in this context
cat -n model_bakery/baker.py | sed -n '975,992p'

# Look for other atomic block usages with database specification
rg "transaction\.atomic\(using" model_bakery/

Repository: model-bakers/model_bakery

Length of output: 1389


🏁 Script executed:

# Find _save_related_objs function definition
rg "def _save_related_objs" model_bakery/

# Look at the function to understand what it does
rg -A 20 "def _save_related_objs" model_bakery/baker.py | head -30

Repository: model-bakers/model_bakery

Length of output: 889


🏁 Script executed:

# Check if Django's QuerySet/Manager has a db attribute or method to get the database
python3 << 'EOF'
# Check Django's QuerySet and Manager API for database tracking
import inspect

# Django QuerySet typically stores the database in _db attribute
print("Looking at common QuerySet attributes related to database:")
print("- _db (private attribute)")
print("- db (property, if it exists)")
print("- get_db() or similar methods")

# The using() method returns a new QuerySet with _db set
# Let's verify the common patterns
EOF

# Also look for how baker._using is defined and used
rg "_using\s*=" model_bakery/baker.py | head -20

Repository: model-bakers/model_bakery

Length of output: 931


🏁 Script executed:

# Check if Django QuerySet has a _db or db attribute
python3 << 'EOF'
# Simulate Django QuerySet behavior
# A QuerySet created with .using(alias) has _db set to that alias
# So manager.using(baker._using) creates a QuerySet with _db=baker._using

# The manager itself doesn't have .db, but the queryset from manager.using() would have _db
# However, we need the database alias passed to transaction.atomic(using=...)

# Let's check if the code accesses _db anywhere
EOF

# Search for _db usage in baker.py to see if it's ever accessed
rg "\._db\b" model_bakery/baker.py

Repository: model-bakers/model_bakery

Length of output: 51


🏁 Script executed:

# Look at the Baker class to understand _using better
rg -B 5 -A 10 "class Baker" model_bakery/baker.py | head -40

# Check if there's any code that tries to access manager.db or similar
rg "manager\." model_bakery/baker.py | grep -v "manager\.bulk_create\|manager\.using"

Repository: model-bakers/model_bakery

Length of output: 780


Wrap writes in atomic block with correct database alias.

Line 987 opens transaction.atomic() on the default connection, so manager.using(baker._using) writes are not protected on non-default databases. Line 978 is also outside the block, meaning related FK rows can be committed even if bulk_create() later fails. Wrap both _save_related_objs() and bulk_create() in transaction.atomic(using=baker._using).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_bakery/baker.py` around lines 986 - 990, The current atomic block uses
the default DB connection and only wraps manager.bulk_create when _full_clean is
true, leaving _save_related_objs and writes on non-default DBs unprotected;
change the flow so that both _save_related_objs(...) and
manager.bulk_create(entries) are executed inside a single
transaction.atomic(using=baker._using) context (regardless of _full_clean) so
that writes use the correct database alias and related FK rows are rolled back
together on failure.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_baker.py (1)

1569-1579: bulk_create rollback test does not currently exercise rollback during insert.

At Line 1574, email="not-an-email" fails in prepare() (full_clean) before bulk_create is reached, so this verifies early validation, not transactional rollback of a failed bulk insert.

Consider either renaming the test to reflect “validate-before-bulk-create” or adding a separate test that fails at DB insert time (e.g., unique constraint collision in a bulk batch) and then asserts zero rows persisted.

♻️ Suggested test adjustment
 class TestFullClean:
@@
-    def test_bulk_create_with_full_clean_rolls_back_on_error(self):
-        """_full_clean=True with _bulk_create=True rolls back all entries on error."""
+    def test_bulk_create_with_full_clean_validates_before_bulk_insert(self):
+        """_full_clean=True with _bulk_create=True validates entries before bulk_create."""
         with pytest.raises(ValidationError):
             baker.make(
                 models.Profile,
                 email="not-an-email",
                 _quantity=5,
                 _bulk_create=True,
                 _full_clean=True,
             )
         assert models.Profile.objects.count() == 0
+
+    `@pytest.mark.django_db`
+    def test_bulk_create_with_full_clean_rolls_back_on_db_error(self):
+        """DB errors during bulk_create should roll back atomically."""
+        baker.make(models.DummyUniqueIntegerFieldModel, value=1)
+        with pytest.raises(Exception):  # tighten to IntegrityError if imported in this file
+            baker.make(
+                models.DummyUniqueIntegerFieldModel,
+                value=iter([2, 1, 3]),  # includes duplicate with existing row
+                _quantity=3,
+                _bulk_create=True,
+                _full_clean=True,
+            )
+        # Only the pre-existing row should remain.
+        assert models.DummyUniqueIntegerFieldModel.objects.count() == 1

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d93c00a7-dec5-45e7-88da-2199390dd60d

📥 Commits

Reviewing files that changed from the base of the PR and between 551811b and c6e5c36.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • docs/basic_usage.md
  • model_bakery/baker.py
  • tests/test_baker.py
✅ Files skipped from review due to trivial changes (2)
  • docs/basic_usage.md
  • model_bakery/baker.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

@benaduo benaduo force-pushed the fix/full-clean-flag branch from c6e5c36 to df33303 Compare March 27, 2026 19:54
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_bakery/baker.py`:
- Around line 161-165: The code is unconditionally forwarding the _full_clean
keyword to custom baker methods and breaks bakers with strict signatures; update
all call sites that invoke baker.make(...) and baker.prepare(...) (references:
baker.make, baker.prepare, and the _save_kwargs/_refresh_after_create
parameters) to only include _full_clean in the kwargs when bool(_full_clean) is
True (i.e., guard by truthiness), and apply this same guard to each of the four
sites mentioned so that _full_clean is omitted for falsey values and preserves
backward compatibility with custom bakers.

In `@tests/test_baker.py`:
- Around line 1568-1579: The test never reaches the new atomic bulk-create
branch because the invalid email causes baker.prepare(...) to raise before the
_bulk_create path; add a new happy-path test using baker.make with valid data
and _bulk_create=True and _full_clean=True (for example create 5 valid models
using baker.make(models.Profile, email="[email protected]", _quantity=5,
_bulk_create=True, _full_clean=True) and assert
models.Profile.objects.count()==5) and also add a second case using an FK-backed
model (use baker.make on the parent, then baker.make(child_model,
parent=parent_instance, _quantity=3, _bulk_create=True, _full_clean=True) and
assert child count and FK links) so the atomic branch in model_bakery.baker (the
_bulk_create/_full_clean transaction path reached via baker.make) is actually
executed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7034ad23-6ae6-4372-8e6e-74b49994ca46

📥 Commits

Reviewing files that changed from the base of the PR and between c6e5c36 and df33303.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • docs/basic_usage.md
  • model_bakery/baker.py
  • tests/test_baker.py
✅ Files skipped from review due to trivial changes (1)
  • docs/basic_usage.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment on lines 161 to 165
baker.make(
_save_kwargs=_save_kwargs,
_refresh_after_create=_refresh_after_create,
_full_clean=_full_clean,
**attrs,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -type f -name "baker.py" 2>/dev/null | head -5

Repository: model-bakers/model_bakery

Length of output: 51


🏁 Script executed:

git ls-files | grep -E "baker\.py$"

Repository: model-bakers/model_bakery

Length of output: 110


🏁 Script executed:

wc -l model_bakery/baker.py

Repository: model-bakers/model_bakery

Length of output: 95


🏁 Script executed:

sed -n '155,180p' model_bakery/baker.py

Repository: model-bakers/model_bakery

Length of output: 699


🏁 Script executed:

sed -n '215,235p' model_bakery/baker.py

Repository: model-bakers/model_bakery

Length of output: 753


🏁 Script executed:

sed -n '965,980p' model_bakery/baker.py

Repository: model-bakers/model_bakery

Length of output: 650


🏁 Script executed:

rg "class Baker" model_bakery/baker.py -A 5

Repository: model-bakers/model_bakery

Length of output: 185


🏁 Script executed:

sed -n '1,100p' model_bakery/baker.py | grep -A 20 "def make"

Repository: model-bakers/model_bakery

Length of output: 283


🏁 Script executed:

rg "def make\(" model_bakery/baker.py -A 10 | head -50

Repository: model-bakers/model_bakery

Length of output: 1446


🏁 Script executed:

rg "def prepare\(" model_bakery/baker.py -A 10 | head -50

Repository: model-bakers/model_bakery

Length of output: 1141


🏁 Script executed:

rg "BAKER_CUSTOM_CLASS" model_bakery/ -B 3 -A 3

Repository: model-bakers/model_bakery

Length of output: 1134


🏁 Script executed:

rg "BAKER_CUSTOM_CLASS" tests/ -B 2 -A 8

Repository: model-bakers/model_bakery

Length of output: 1374


🏁 Script executed:

find . -name "*.md" -o -name "*.rst" | xargs grep -l "BAKER_CUSTOM_CLASS" 2>/dev/null

Repository: model-bakers/model_bakery

Length of output: 97


🏁 Script executed:

rg "class.*Baker" tests/ -A 10 | head -80

Repository: model-bakers/model_bakery

Length of output: 5037


🏁 Script executed:

rg "def make\|def prepare" tests/ -B 2 -A 5 | grep -A 5 "class" | head -50

Repository: model-bakers/model_bakery

Length of output: 51


🏁 Script executed:

sed -n '1,50p' tests/test_extending_bakery.py

Repository: model-bakers/model_bakery

Length of output: 1369


🏁 Script executed:

rg "ClassWithoutMake|ClassWithoutPrepare" tests/test_extending_bakery.py -B 3 -A 5

Repository: model-bakers/model_bakery

Length of output: 673


🏁 Script executed:

git log --oneline -- model_bakery/baker.py | head -20

Repository: model-bakers/model_bakery

Length of output: 143


🏁 Script executed:

git log --oneline -S "_full_clean" -- model_bakery/baker.py | head -10

Repository: model-bakers/model_bakery

Length of output: 143


🏁 Script executed:

git show df33303 --stat

Repository: model-bakers/model_bakery

Length of output: 4558


🏁 Script executed:

git show df33303 | head -200

Repository: model-bakers/model_bakery

Length of output: 5683


🏁 Script executed:

rg "_full_clean" model_bakery/baker.py | wc -l

Repository: model-bakers/model_bakery

Length of output: 71


🏁 Script executed:

sed -n '160,176p' model_bakery/baker.py

Repository: model-bakers/model_bakery

Length of output: 510


🏁 Script executed:

sed -n '155,180p' model_bakery/baker.py | cat -n

Repository: model-bakers/model_bakery

Length of output: 881


🏁 Script executed:

sed -n '215,235p' model_bakery/baker.py | cat -n

Repository: model-bakers/model_bakery

Length of output: 900


🏁 Script executed:

sed -n '965,985p' model_bakery/baker.py | cat -n

Repository: model-bakers/model_bakery

Length of output: 925


🏁 Script executed:

rg "def make" model_bakery/baker.py -A 25 | grep -A 25 "def make.*self"

Repository: model-bakers/model_bakery

Length of output: 51


🏁 Script executed:

rg "^\s+def make\(" model_bakery/baker.py -A 20

Repository: model-bakers/model_bakery

Length of output: 815


🏁 Script executed:

rg "^\s+def prepare\(" model_bakery/baker.py -A 20

Repository: model-bakers/model_bakery

Length of output: 731


Guard _full_clean behind truthiness to maintain backward compatibility with custom bakers.

Custom bakers need only implement make() and prepare() methods with no required parameters beyond self. The code unconditionally passes _full_clean to these methods at four call sites (lines 161–165, 170–174, 222–226, 971–974), which breaks existing custom bakers with strict signatures. Only pass _full_clean when it is truthy to preserve compatibility:

Compatibility-safe forwarding pattern
-    return baker.prepare(_save_related=_save_related, _full_clean=_full_clean, **attrs)
+    prepare_kwargs = {"_save_related": _save_related, **attrs}
+    if _full_clean:
+        prepare_kwargs["_full_clean"] = True
+    return baker.prepare(**prepare_kwargs)

Apply the same guard to all four _full_clean forwarding sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_bakery/baker.py` around lines 161 - 165, The code is unconditionally
forwarding the _full_clean keyword to custom baker methods and breaks bakers
with strict signatures; update all call sites that invoke baker.make(...) and
baker.prepare(...) (references: baker.make, baker.prepare, and the
_save_kwargs/_refresh_after_create parameters) to only include _full_clean in
the kwargs when bool(_full_clean) is True (i.e., guard by truthiness), and apply
this same guard to each of the four sites mentioned so that _full_clean is
omitted for falsey values and preserves backward compatibility with custom
bakers.

@benaduo
Copy link
Copy Markdown
Contributor Author

benaduo commented Mar 27, 2026

Hi @amureki, just wanted to flag a few things for your review:

  • This is a fresh implementation of the feature requested in Feature Request: Add _full_clean:bool=False to baker.make() #523, based on the approach from Issue 523 #547 (thanks @berinhard). I've incorporated all the feedback from your review of that PR directly into this implementation.

  • The _full_clean flag is opt-in (False by default) so it's fully backwards compatible , no existing code is affected.

  • For the _bulk_create=True path, I wrapped both _save_related_objs() and manager.bulk_create() inside transaction.atomic(using=baker._using) so related FK rows and the bulk insert roll back together, and the correct database alias is used for non-default databases.

  • 5 tests cover: valid creation, invalid raises ValidationError, no DB entry on error, prepare path, and bulk_create rollback.

Happy to make any changes. Thanks for your time.

@benaduo benaduo force-pushed the fix/full-clean-flag branch from df33303 to a4d2134 Compare April 1, 2026 19:40
Fixes model-bakers#523. Adds a _full_clean=False opt-in parameter across all main
creation functions. When True, calls Django's full_clean() on each
instance before saving. For _bulk_create=True, validation runs at
prepare() time and the bulk_create call is wrapped in transaction.atomic()
to ensure atomic rollback if validation fails.

Based on the approach in model-bakers#547 (thanks @berinhard), incorporating feedback
from Rust's review of that PR.
@benaduo benaduo force-pushed the fix/full-clean-flag branch from a4d2134 to d70a2b5 Compare April 1, 2026 23:13
@benaduo
Copy link
Copy Markdown
Contributor Author

benaduo commented Apr 1, 2026

Hi @amureki, this PR is ready for review. All CI checks are passing.

Quick summary of what's in here:

  • _full_clean=False opt-in flag across make(), prepare(), and bulk_create()
  • When True, calls Django's full_clean() before saving
  • _bulk_create=True + _full_clean=True wraps everything in transaction.atomic(using=baker._using or None) for correct rollback on both default and non-default databases
  • 6 tests covering valid creation, invalid raises, no DB entry on error, prepare path, bulk_create rollback, and happy-path bulk_create

Happy to make any changes based on your feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add _full_clean:bool=False to baker.make()

1 participant