diff --git a/Makefile b/Makefile index 739be3a..9af5098 100644 --- a/Makefile +++ b/Makefile @@ -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/* diff --git a/django_pydantic_field/v1/fields.py b/django_pydantic_field/v1/fields.py index 24a7af0..22a7850 100644 --- a/django_pydantic_field/v1/fields.py +++ b/django_pydantic_field/v1/fields.py @@ -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__( @@ -61,8 +67,10 @@ def __copy__(self): return copied def get_default(self): - value = super().get_default() - return self.to_python(value) + default_value = super().get_default() + if self.has_default(): + return self.to_python(default_value) + return default_value def to_python(self, value) -> "base.SchemaT": # Attempt to resolve forward referencing schema if it was not succesful @@ -104,6 +112,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) @@ -162,8 +176,7 @@ def _prepare_model_schema(self, cls=None): def _deconstruct_default(self, kwargs): default = kwargs.get("default", NOT_PROVIDED) - - if not (default is NOT_PROVIDED or callable(default)): + if default is not NOT_PROVIDED and not callable(default): if self._is_prepared_schema: default = self.get_prep_value(default) kwargs.update(default=default) @@ -202,6 +215,6 @@ def SchemaField( ) -> "base.ST": ... -def SchemaField(schema=None, config=None, default=None, *args, **kwargs) -> t.Any: +def SchemaField(schema=None, config=None, default=NOT_PROVIDED, *args, **kwargs) -> t.Any: # type: ignore kwargs.update(schema=schema, config=config, default=default) return PydanticSchemaField(*args, **kwargs) diff --git a/django_pydantic_field/v2/fields.py b/django_pydantic_field/v2/fields.py index 7113342..a9d7cfe 100644 --- a/django_pydantic_field/v2/fields.py +++ b/django_pydantic_field/v2/fields.py @@ -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__( @@ -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) @@ -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")) diff --git a/tests/test_fields.py b/tests/test_fields.py index f6bd24e..9f57ffa 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -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")