Skip to content

Commit 6976add

Browse files
authored
Merge pull request #22 from maykinmedia/clarify-default-behavior-docs
Clarify behavior around field defaults in the docs
2 parents c3cb480 + 38ca36f commit 6976add

File tree

3 files changed

+102
-10
lines changed

3 files changed

+102
-10
lines changed

README.rst

+57-9
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,17 @@ Key Concepts
6363
- **Configuration Model**: A `Pydantic <https://docs.pydantic.dev/>`_ model defining the structure and validation rules for your configuration.
6464
- **Configuration Step**: A class that implements the actual configuration logic using the validated configuration model.
6565

66-
Getting Started
67-
---------------
6866

6967
Define a Configuration Model
70-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
68+
----------------------------
7169

7270
.. code-block:: python
7371
7472
from pydantic import Field
7573
from django_setup_configuration import ConfigurationModel, DjangoModelRef
7674
7775
class UserConfigurationModel(ConfigurationModel):
78-
# Use Pydantic's validation features
76+
# A regular Pydantic field
7977
add_to_groups: list[str] = Field(
8078
default_factory=list,
8179
description="Groups to add the user to"
@@ -95,8 +93,58 @@ Define a Configuration Model
9593
User: ["password"]
9694
}
9795
96+
97+
Field Defaults
98+
^^^^^^^^^^^^^^
99+
100+
For regular Pydantic fields, you must explicitly configure defaults using `Field
101+
(default=...)` or `Field(default_factory=lambda: ...)` as specified in the `Pydantic
102+
documentation <https://docs.pydantic.dev/2.10/concepts/fields/#default-values>`_.
103+
104+
**NOTE:** Marking a field as ``Optional`` or using ``... | None`` does *not* automatically
105+
set the field's default to `None`. You must set this explicitly if you want the field to
106+
be optional:
107+
108+
.. code-block:: python
109+
110+
from pydantic import Field
111+
112+
class ConfigModel(ConfigurationModel):
113+
optional_field: int | None = DjangoModelRef(SomeModel, "some_field", default=None)
114+
115+
For ``DjangoModelRef``, the default value handling follows these rules:
116+
117+
You can provide explicit defaults using the ``default`` or ``default_factory`` kwargs,
118+
similar to regular Pydantic fields:
119+
120+
.. code-block:: python
121+
122+
class ConfigModel(ConfigurationModel):
123+
# Explicit string default
124+
field_with_explicit_default = DjangoModelRef(SomeModel, "some_field", default="foobar")
125+
126+
# Explicit default factory for a list
127+
field_with_explicit_default_factory: list[str] = DjangoModelRef(
128+
SomeModel, "some_other_field", default_factory=list
129+
)
130+
131+
When no explicit default is provided, the default is derived from the referenced Django field:
132+
133+
1. If the Django field has an explicit default, that default will be used.
134+
135+
2. If no explicit default is set but the field has ``null=True`` set:
136+
137+
a. The default will be set to ``None``
138+
b. The field will be optional
139+
140+
3. If no explicit default is provided and the field is not nullable, but has ``blank=True`` **and** it is a string-type field:
141+
142+
a. The default will be an empty string
143+
b. The field will be optional
144+
145+
98146
Create a Configuration Step
99-
^^^^^^^^^^^^^^^^^^^^^^^^^^^
147+
---------------------------
100148

101149
.. code-block:: python
102150
@@ -130,8 +178,8 @@ Create a Configuration Step
130178
group = Group.objects.get(name=group_name)
131179
group.user_set.add(user)
132180
133-
Configuration File
134-
^^^^^^^^^^^^^^^^^^
181+
Configuration Source
182+
--------------------
135183

136184
Create a YAML configuration file with your settings:
137185

@@ -155,7 +203,7 @@ keys are exclusively used for the steps' ``enable_setting`` key, and the ``names
155203
key which encapsulates the configuration model's attributes.
156204

157205
Step Registration
158-
^^^^^^^^^^^^^^^^^
206+
-----------------
159207

160208
Register your configuration steps in Django settings:
161209

@@ -258,7 +306,7 @@ Using Test Helpers
258306
# Add assertions
259307
260308
Best Practices
261-
--------------
309+
==============
262310

263311
- **Idempotency**: Design steps that can be run multiple times without unintended side effects.
264312
- **Validation**: You can use the full range of Pydantic's validation capabilities.

testapp/models.py

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class TestModel(models.Model):
1010
required_int = models.IntegerField()
1111
int_with_default = models.IntegerField(default=42)
1212
nullable_int = models.IntegerField(null=True)
13+
nullable_int_with_default = models.IntegerField(default=42)
1314
nullable_str = models.CharField(null=True, blank=False, max_length=1)
1415
nullable_and_blank_str = models.CharField(null=True, blank=False, max_length=1)
1516
blank_str = models.CharField(null=False, blank=True, max_length=1)

tests/test_django_model_ref_field.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ class Meta:
7777
assert field.is_required() is False
7878

7979

80+
def test_explicit_default_overrides_model_field_default():
81+
82+
class Config(ConfigurationModel):
83+
int_with_default = DjangoModelRef(TestModel, "int_with_default")
84+
int_with_overridden_default = DjangoModelRef(
85+
TestModel, "int_with_default", default=1874
86+
)
87+
88+
int_with_default_field = Config.model_fields["int_with_default"]
89+
int_with_overridden_default_field = Config.model_fields[
90+
"int_with_overridden_default"
91+
]
92+
93+
assert int_with_default_field.default == 42
94+
assert int_with_overridden_default_field.default == 1874
95+
96+
assert (
97+
int_with_default_field.annotation
98+
== int_with_overridden_default_field.annotation
99+
== int
100+
)
101+
assert (
102+
int_with_default_field.is_required()
103+
is int_with_overridden_default_field.is_required()
104+
is False
105+
)
106+
107+
80108
def test_null_is_true_sets_default_to_none():
81109

82110
class Config(ConfigurationModel):
@@ -92,6 +120,21 @@ class Meta:
92120
assert field.is_required() is False
93121

94122

123+
def test_null_prefers_explicit_default():
124+
125+
class Config(ConfigurationModel):
126+
class Meta:
127+
django_model_refs = {TestModel: ["nullable_int_with_default"]}
128+
129+
field = Config.model_fields["nullable_int_with_default"]
130+
131+
assert field.title == "nullable int with default"
132+
assert field.description is None
133+
assert field.annotation == int
134+
assert field.default == 42
135+
assert field.is_required() is False
136+
137+
95138
def test_null_is_true_sets_default_to_none_for_str_fields():
96139

97140
class Config(ConfigurationModel):
@@ -107,7 +150,7 @@ class Meta:
107150
assert field.is_required() is False
108151

109152

110-
def test_blank_is_true_null_is_false_sets_default_to_none_for_str_fields():
153+
def test_blank_is_true_null_is_false_sets_default_to_empty_str_for_str_fields():
111154
class Config(ConfigurationModel):
112155
class Meta:
113156
django_model_refs = {TestModel: ["blank_str"]}

0 commit comments

Comments
 (0)