Repository: django-import-export/django-import-export

Issue:
V4: undocumented interface changes with not helpful errors
**Describe the bug**
The parameters of `before_import` and `before_save_instance` changed.
I don't see any mention of that in the new changelog file for V4 and the error messages didn't help me very much.

**To Reproduce**
Steps to reproduce the behavior:
1. Clone `django-hordak` at commit i.e. [0108808](https://github.com/adamcharnock/django-hordak/commit/0108808204fd41e40f7f4d7dfa05ec440e1070cc)
2. Install requirements
3. Install django-import-export in v4.0.0b2
4. Fix #1730 by adding `import_id_fields = ()` to the StatementLineResource
4. Run `LC_NUMERIC="en_GB" coverage run ./manage.py test hordak.tests.views.test_statement_csv_import.DryRunViewTestCase.test_post`

The error output is:

```
======================================================================
FAIL: test_post (hordak.tests.views.test_statement_csv_import.DryRunViewTestCase.test_post)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/petr/soubory/programovani/blenderkit/django-hordak/hordak/tests/views/test_statement_csv_import.py", line 64, in test_post
    self.assertEqual(len(result.failed_dataset), 0, result.failed_dataset.dict)
AssertionError: 1 != 0 : [OrderedDict([('col_1', '1'), ('date', '1/1/2000'), ('col_3', '123456789'), ('amount', '123'), ('col_5', 'OTH'), ('description', 'Some random notes'), ('Error', "'similar_total'")])]
```

**Versions (please complete the following information):**
 - Django Import Export: 4.0.0b2
 - Python 3.11
 - Django 5.0

**Expected behavior**
The problem is, that the root error is overriden by error in `skip_row` function which fails because `before_import` is not called and `similar_total` column is not added.

It can be fixed by changing `before_import` and `before_save_instance` headers from:
```python
    def before_import(self, dataset, using_transactions, dry_run, **kwargs):
    def before_save_instance(self, instance, using_transactions, dry_run):
```
to
```python
    def before_import(self, dataset, **kwargs):
    def before_save_instance(self, instance, row, **kwargs):
```

I would expect to see some update guidelines in the changelog regarding this incompatible change.


Discussion:
> I would expect to see some update guidelines in the changelog regarding this incompatible change.

Thanks for raising and testing v4.  Release notes are [here](https://django-import-export.readthedocs.io/en/4.0.0-beta.2/release_notes.html) (linked from changelog).  The release notes describe all interface changes.
Oh sorry, I didn't see that. I was clicking on the link on GitHub and it stayed on the same page.
I bumped into another similar problem regarding `import_obj->import_instance`. The problem is, that `django-import-export` silently stops using the old `import_obj`.
What about detecting if `import_obj` is present and if yes throwing exception directly?
Good idea - it should be easy to add the old method name and raise a deprecation warning.

Resolving patch:
diff --git a/import_export/resources.py b/import_export/resources.py
--- a/import_export/resources.py
+++ b/import_export/resources.py
@@ -4,6 +4,7 @@
 from collections import OrderedDict
 from copy import deepcopy
 from html import escape
+from warnings import warn
 
 import tablib
 from diff_match_patch import diff_match_patch
@@ -423,6 +424,18 @@ def import_field(self, field, instance, row, is_m2m=False, **kwargs):
     def get_import_fields(self):
         return [self.fields[f] for f in self.get_import_order()]
 
+    def import_obj(self, obj, data, dry_run, **kwargs):
+        warn(
+            "The 'import_obj' method is deprecated and will be replaced "
+            "with 'import_instance(self, instance, row, **kwargs)' "
+            "in a future release.  Refer to Release Notes for details.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        if dry_run is True:
+            kwargs.update({"dry_run": dry_run})
+        self.import_instance(obj, data, **kwargs)
+
     def import_instance(self, instance, row, **kwargs):
         r"""
         Traverses every field in this Resource and calls
@@ -615,6 +628,18 @@ def after_import_row(self, row, row_result, **kwargs):
         """
         pass
 
+    def after_import_instance(self, instance, new, row_number=None, **kwargs):
+        warn(
+            "The 'after_import_instance' method is deprecated and will be replaced "
+            "with 'after_init_instance(self, instance, new, row, **kwargs)' "
+            "in a future release.  Refer to Release Notes for details.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        if row_number is not None:
+            kwargs.update({"row_number": row_number})
+        self.after_init_instance(instance, new, None, **kwargs)
+
     def after_init_instance(self, instance, new, row, **kwargs):
         r"""
         Override to add additional logic. Does nothing by default.


Test patch:
diff --git a/tests/core/tests/test_resources/test_resources.py b/tests/core/tests/test_resources/test_resources.py
--- a/tests/core/tests/test_resources/test_resources.py
+++ b/tests/core/tests/test_resources/test_resources.py
@@ -1,5 +1,6 @@
 import json
 import sys
+import warnings
 from collections import OrderedDict
 from datetime import date
 from decimal import Decimal, InvalidOperation
@@ -1886,3 +1887,59 @@ class Meta:
 
     def test_resource_gets_correct_model_from_string(self):
         self.assertEqual(self.resource._meta.model, Book)
+
+
+class DeprecatedMethodTest(TestCase):
+    """
+    These tests relate to renamed methods in v4.
+    The tests can be removed when the deprecated methods are removed.
+    """
+
+    def setUp(self):
+        rows = [
+            ["1", "Ulysses"],
+        ]
+        self.dataset = tablib.Dataset(*rows, headers=["id", "name"])
+        self.obj = Book.objects.create(id=1, name="Ulysses")
+
+    def test_import_obj_renamed(self):
+        resource = BookResource()
+        with self.assertWarns(
+            DeprecationWarning,
+        ):
+            resource.import_obj(self.obj, self.dataset, dry_run=True)
+
+    def test_import_obj_passes_params(self):
+        class MyBookResource(resources.ModelResource):
+            def import_instance(self, instance, row, **kwargs):
+                self.kwargs = kwargs
+
+            class Meta:
+                model = Book
+
+        resource = MyBookResource()
+        with warnings.catch_warnings():
+            warnings.filterwarnings("ignore", category=DeprecationWarning)
+            resource.import_obj(self.obj, self.dataset, True)
+        self.assertTrue(resource.kwargs["dry_run"])
+
+    def test_after_import_instance_renamed(self):
+        resource = BookResource()
+        with self.assertWarns(
+            DeprecationWarning,
+        ):
+            resource.after_import_instance(self.obj, True, row_number=1)
+
+    def test_after_import_instance_passes_params(self):
+        class MyBookResource(resources.ModelResource):
+            def after_init_instance(self, instance, new, row, **kwargs):
+                self.kwargs = kwargs
+
+            class Meta:
+                model = Book
+
+        resource = MyBookResource()
+        with warnings.catch_warnings():
+            warnings.filterwarnings("ignore", category=DeprecationWarning)
+            resource.after_import_instance(self.obj, True, row_number=1)
+        self.assertEqual(1, resource.kwargs["row_number"])