Skip to content

Commit

Permalink
Merge pull request #33 from saritasa-nest/feature/fix-pickling-issue
Browse files Browse the repository at this point in the history
Fix pickling issue
  • Loading branch information
yalef committed May 17, 2024
2 parents 387404e + 5b112ef commit 86fa77a
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 10 deletions.
2 changes: 0 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.7'

services:
redis:
image: redis:5
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.7 on 2024-05-16 09:21

from django.db import migrations
import import_export.results
import picklefield.fields


class Migration(migrations.Migration):

dependencies = [
("import_export_extensions", "0006_importjob_input_errors_file"),
]

operations = [
migrations.AlterField(
model_name="exportjob",
name="result",
field=picklefield.fields.PickledObjectField(
default=import_export.results.Result,
editable=False,
help_text="Internal job result object that contain info about job statistics. Pickled Python object",
verbose_name="Job result",
),
),
migrations.AlterField(
model_name="importjob",
name="result",
field=picklefield.fields.PickledObjectField(
default=import_export.results.Result,
editable=False,
help_text="Internal job result object that contain info about job statistics. Pickled Python object",
verbose_name="Job result",
),
),
]
3 changes: 2 additions & 1 deletion import_export_extensions/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.utils import module_loading
from django.utils.translation import gettext_lazy as _

from import_export.results import Result
from picklefield.fields import PickledObjectField


Expand Down Expand Up @@ -97,7 +98,7 @@ class BaseJob(TimeStampedModel):
help_text=_("User which started job"),
)
result = PickledObjectField(
default=str,
default=Result,
verbose_name=_("Job result"),
help_text=_(
"Internal job result object that contain "
Expand Down
56 changes: 53 additions & 3 deletions import_export_extensions/results.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import collections
import typing

from django.core.exceptions import ValidationError

from import_export import results
Expand All @@ -23,10 +26,57 @@ def __reduce__(self):

class RowResult(results.RowResult):
"""Custom row result class with ability to store skipped errors in row."""
def __init__(self):
self.non_field_skipped_errors: list[Error] = []

def __init__(self) -> None:
"""Copy of base init except creating validation error field."""
self.non_field_skipped_errors: list[results.Error] = []
self.field_skipped_errors: dict[str, list[ValidationError]] = dict()
super().__init__()
self.errors: list[Error] = []
self.diff: list[str] | None = None
self.import_type = None
self.row_values: dict[str, typing.Any] = {}
self.object_id = None
self.object_repr = None
self.instance = None
self.original = None
self.new_record = None
# variable to store modified value of ValidationError
self._validation_error: ValidationError | None = None

@property
def validation_error(self) -> ValidationError | None:
"""Return modified validation error."""
return self._validation_error

@validation_error.setter
def validation_error(self, value: ValidationError) -> None:
"""Modify passed error to reduce count of nested exception classes.
Result class is saved in `ImportJob` model as pickled field
and python's `pickle` can't handle
`ValidationError` with high level of nested errors,
therefore we reduce nesting by using string value of nested errors.
If `ValidationError` has no `message_dict` attr,
then it means that there're no nested exceptions
and we can safely save it.
Otherwise, we will go through all nested validation errors
and build new `ValidationError` with only one level of
nested `ValidationError` instances.
"""
if not hasattr(value, "message_dict"):
self._validation_error = value
return
result = collections.defaultdict(list)
for field, error_messages in value.message_dict.items():
validation_errors = [
ValidationError(message=message, code="invalid")
for message in error_messages
]
result[field].extend(validation_errors)
self._validation_error = ValidationError(result)

@property
def has_skipped_errors(self) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion tests/fake_app/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Migration(migrations.Migration):
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("name", models.CharField(max_length=100, unique=True)),
],
options={
'verbose_name': 'Artist',
Expand Down
2 changes: 1 addition & 1 deletion tests/fake_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __str__(self) -> str:

class Artist(models.Model):
"""Model representing artist."""
name = models.CharField(max_length=100)
name = models.CharField(max_length=100, unique=True)
bands = models.ManyToManyField("Band", through="Membership")

instrument = models.ForeignKey(
Expand Down
3 changes: 2 additions & 1 deletion tests/fake_app/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class SimpleArtistResource(CeleryModelResource):

class Meta:
model = Artist
clean_model_instances = True
fields = [
"id",
"name",
Expand All @@ -34,5 +35,5 @@ class ArtistResourceWithM2M(CeleryModelResource):

class Meta:
model = Artist
fields = ["id", "name", "bands", "instrument"]
clean_model_instances = True
fields = ["id", "name", "bands", "instrument"]
15 changes: 14 additions & 1 deletion tests/test_models/test_import/test_import_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from import_export_extensions.models import ImportJob

from ...fake_app.factories import ArtistImportJobFactory
from ...fake_app.factories import ArtistFactory, ArtistImportJobFactory
from ...fake_app.models import Artist


Expand Down Expand Up @@ -143,3 +143,16 @@ def test_force_import_create_correct_rows(
import_job.refresh_from_db()
assert import_job.import_status == import_job.ImportStatus.IMPORTED
assert Artist.objects.filter(name=new_artist.name).exists()


def test_import_data_with_validation_error(existing_artist: Artist):
"""Test import handles `ValidationError` instances correctly.
If validation error occur, then job should end with `INPUT_ERROR` status.
"""
wrong_artist = ArtistFactory.build(name=existing_artist.name)
job = ArtistImportJobFactory(artists=[wrong_artist])
job.parse_data()
job.refresh_from_db()
assert job.import_status == ImportJob.ImportStatus.INPUT_ERROR

0 comments on commit 86fa77a

Please sign in to comment.