Skip to content

Commit

Permalink
Merge pull request #36 from maykinmedia/31-literal-default-values
Browse files Browse the repository at this point in the history
[#31] Bypass validation of defaults for Django fields containing choices
  • Loading branch information
swrichards authored Dec 17, 2024
2 parents 388dc94 + fab1ad8 commit 751557a
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 1 deletion.
16 changes: 15 additions & 1 deletion django_setup_configuration/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ def __init__(
else:
field_info_creation_kwargs["default"] = inferred_default

# Defaults for fields with choices often do not map neatly onto the consructed
# type used for serialization (e.g. a string) because they may not be a literal
# choices but e.g. an enum/Choices member. Inferring the types of the default
# can be non-trivial (especially if a default factory is involved), and because
# we care about types that can be expressed as simple YAML/JSON scalars, it also
# would not make much sense to add complex types to the annotation.
validate_defaults = False if self.django_field.choices else True
field_info_creation_kwargs["validate_default"] = field_info_creation_kwargs[
"validate_return"
] = validate_defaults

return super().__init__(**field_info_creation_kwargs)

@staticmethod
Expand All @@ -135,7 +146,10 @@ def _get_python_type(
):
"""Map Django field types to Python types."""
if choices := getattr(django_field, "choices"):
choice_values = tuple(choice[0] for choice in choices)
choice_values = tuple(
choice[0]
for choice in (choices if not callable(choices) else choices())
)
return Literal[choice_values]

mapping: Mapping[
Expand Down
10 changes: 10 additions & 0 deletions testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class TestModel(models.Model):
str_with_choices_and_default = models.CharField(
max_length=3, choices=StrChoices.choices, default=StrChoices.bar
)
str_with_choices_and_incorrectly_typed_default = models.CharField(
max_length=3, choices=StrChoices.choices, default=1974
)
str_with_choices_and_incorrectly_typed_default_factory = models.CharField(
max_length=3, choices=StrChoices.choices, default=lambda: 1985
)
str_with_choices_and_blank = models.CharField(
max_length=3, choices=StrChoices.choices, blank=True
)
Expand All @@ -42,3 +48,7 @@ class TestModel(models.Model):
int_with_choices_and_blank_and_non_choice_default = models.IntegerField(
blank=True, choices=((1, "FOO"), (8, "BAR")), default=42
)

int_with_choices_callable = models.IntegerField(
choices=lambda: ((1, "FOO"), (8, "BAR"))
)
31 changes: 31 additions & 0 deletions tests/test_django_model_ref_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,22 @@ class Config(ConfigurationModel):
assert field.is_required() is True


def test_int_with_choices_callable_has_literal_annotation():

class Config(ConfigurationModel):
int_with_choices_callable = DjangoModelRef(
TestModel, "int_with_choices_callable"
)

field = Config.model_fields["int_with_choices_callable"]

assert field.title == "int with choices callable"
assert field.description is None
assert field.annotation == Literal[1, 8]
assert field.default == PydanticUndefined
assert field.is_required() is True


def test_int_with_choices_and_override_has_overridden_annotation():

class Config(ConfigurationModel):
Expand Down Expand Up @@ -356,3 +372,18 @@ class Config(ConfigurationModel):
assert field.annotation == Literal[1, 8] | Literal[42]
assert field.default == 42
assert field.is_required() is False


def test_choices_with_incorrectly_typed_default_is_not_validated():

class Config(ConfigurationModel):
str_with_choices_and_incorrectly_typed_default = DjangoModelRef(
TestModel, "str_with_choices_and_incorrectly_typed_default"
)
str_with_choices_and_incorrectly_typed_default_factory = DjangoModelRef(
TestModel, "str_with_choices_and_incorrectly_typed_default_factory"
)

config = Config()
assert config.str_with_choices_and_incorrectly_typed_default == 1974
assert config.str_with_choices_and_incorrectly_typed_default_factory == 1985

0 comments on commit 751557a

Please sign in to comment.