Skip to content

Commit

Permalink
Bypass field validation on model instantiation if it does not contain…
Browse files Browse the repository at this point in the history
… a default= argument
  • Loading branch information
surenkov committed May 5, 2024
1 parent b6bb473 commit 77e2732
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 6 deletions.
17 changes: 14 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
DJANGO_SETTINGS_MODULE ?= "tests.settings.django_test_settings"

.PHONY: install build test lint upload upload-test clean
export DJANGO_SETTINGS_MODULE=tests.settings.django_test_settings

.PHONY: install
install:
python3 -m pip install build twine
python3 -m pip install -e .[dev,test]

.PHONY: build
build:
python3 -m build

.PHONY: migrations
migrations:
python3 -m django makemigrations --noinput

.PHONY: runserver
runserver:
python3 -m django migrate && \
python3 -m django runserver

.PHONY: check
check:
python3 -m django check

.PHONY: test
test: A=
test:
pytest $(A)

.PHONY: lint
lint: A=.
lint:
python3 -m mypy $(A)

.PHONY: upload
upload:
python3 -m twine upload dist/*

.PHONY: upload-test
upload-test:
python3 -m twine upload --repository testpypi dist/*

.PHONY: clean
clean:
rm -rf dist/*
14 changes: 13 additions & 1 deletion django_pydantic_field/v1/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,14 @@ def __set__(self, obj, value):
obj.__dict__[self.field.attname] = self.field.to_python(value)


class UninitializedSchemaAttribute(SchemaAttribute):
def __set__(self, obj, value):
if value is not None:
value = self.field.to_python(value)
obj.__dict__[self.field.attname] = value


class PydanticSchemaField(JSONField, t.Generic[base.ST]):
descriptor_class = SchemaAttribute
_is_prepared_schema: bool = False

def __init__(
Expand Down Expand Up @@ -104,6 +110,12 @@ def deconstruct(self):

return name, path, args, kwargs

@staticmethod
def descriptor_class(field: "PydanticSchemaField") -> DeferredAttribute:
if field.has_default():
return SchemaAttribute(field)
return UninitializedSchemaAttribute(field)

def contribute_to_class(self, cls, name, private_only=False):
if self.schema is None:
self._resolve_schema_from_type_hints(cls, name)
Expand Down
17 changes: 15 additions & 2 deletions django_pydantic_field/v2/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,14 @@ def __set__(self, obj, value):
obj.__dict__[self.field.attname] = self.field.to_python(value)


class UninitializedSchemaAttribute(SchemaAttribute):
def __set__(self, obj, value):
if value is not None:
value = self.field.to_python(value)
obj.__dict__[self.field.attname] = value


class PydanticSchemaField(JSONField, ty.Generic[types.ST]):
descriptor_class: type[DeferredAttribute] = SchemaAttribute
adapter: types.SchemaAdapter

def __init__(
Expand Down Expand Up @@ -102,6 +108,12 @@ def deconstruct(self) -> ty.Any:

return field_name, import_path, args, kwargs

@staticmethod
def descriptor_class(field: PydanticSchemaField) -> DeferredAttribute:
if field.has_default():
return SchemaAttribute(field)
return UninitializedSchemaAttribute(field)

def contribute_to_class(self, cls: types.DjangoModelType, name: str, private_only: bool = False) -> None:
self.adapter.bind(cls, name)
super().contribute_to_class(cls, name, private_only)
Expand All @@ -118,7 +130,8 @@ def check(self, **kwargs: ty.Any) -> list[checks.CheckMessage]:

try:
# Test that the default value conforms to the schema.
self.get_prep_value(self.get_default())
if self.has_default():
self.get_prep_value(self.get_default())
except pydantic.ValidationError as exc:
message = f"Default value cannot be adapted to the schema. Pydantic error: \n{str(exc)}"
performed_checks.append(checks.Error(message, obj=self, id="pydantic.E002"))
Expand Down
7 changes: 7 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,10 @@ def test_copy_field():
assert copied.name == Building.meta.field.name
assert copied.attname == Building.meta.field.attname
assert copied.concrete == Building.meta.field.concrete


def test_model_init_no_default():
try:
SampleModel()
except Exception:
pytest.fail("Model with schema field without a default value should be able to initialize")

0 comments on commit 77e2732

Please sign in to comment.