Repository: 3YOURMIND/django-migration-linter (python)

Issue:
Linter failing when using django 'through'
### through doc
https://docs.djangoproject.com/en/4.0/ref/models/fields/#django.db.models.ManyToManyField.through

### Example code
```
def forwards_func(apps, schema_editor):
    Question = apps.get_model("solution", "Question")
    ...
    Question.many_to_may.through.objects.bulk_create(...)                 <- this line?
    ...
```
### Example Error
```
(fs_solution, 0002_my_migration)... ERR (cached)
        'forwards_func': Could not find an 'apps.get_model("...", "through")' call. Importing the model directly is incorrect for data migrations.
```


Pull request description:
Fix #218 - Handle model 'objects' call with other parts in the name.
Fix #218

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a1b5213..300fe00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 4.1.1 (unreleased)
+
+- Fixed `RunPython` model import check when using a `through` object like `MyModel.many_to_many.through.objects.filter(...)` (issue #218)
+
 ## 4.1.0
 
 - Allow configuring logging for `makemigrations` command and unify behaviour with `lintmigrations` (issue #207)
diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py
index f4b1695..99e72f9 100644
--- a/django_migration_linter/migration_linter.py
+++ b/django_migration_linter/migration_linter.py
@@ -531,7 +531,7 @@ class MigrationLinter(object):
 
     @staticmethod
     def get_runpython_model_import_issues(code):
-        model_object_regex = re.compile(r"[^a-zA-Z]?([a-zA-Z0-9]+?)\.objects")
+        model_object_regex = re.compile(r"[^a-zA-Z0-9._]?([a-zA-Z0-9._]+?)\.objects")
 
         function_name = code.__name__
         source_code = inspect.getsource(code)
@@ -539,6 +539,7 @@ class MigrationLinter(object):
         called_models = model_object_regex.findall(source_code)
         issues = []
         for model in called_models:
+            model = model.split(".", 1)[0]
             has_get_model_call = (
                 re.search(
                     r"{}.*= +\w+\.get_model\(".format(model),


Test patch:
diff --git a/tests/functional/test_data_migrations.py b/tests/functional/test_data_migrations.py
index 5478eff..2bb039c 100644
--- a/tests/functional/test_data_migrations.py
+++ b/tests/functional/test_data_migrations.py
@@ -137,7 +137,7 @@ class DataMigrationModelImportTestCase(unittest.TestCase):
 
     def test_not_overlapping_model_name(self):
         """
-        Correct for the import error, but should raise a warning
+        Correct for the import error, but should raise a warning.
         """
 
         def forward_method(apps, schema_editor):
@@ -159,7 +159,7 @@ class DataMigrationModelImportTestCase(unittest.TestCase):
 
     def test_not_overlapping_one_param(self):
         """
-        Not an error, but should raise a warning
+        Not an error, but should raise a warning.
         """
 
         def forward_method(apps, schema_editor):
@@ -170,6 +170,24 @@ class DataMigrationModelImportTestCase(unittest.TestCase):
         issues = MigrationLinter.get_runpython_model_import_issues(forward_method)
         self.assertEqual(0, len(issues))
 
+    def test_m2m_through_orm_usage(self):
+        def forward_method(apps, schema_editor):
+            MyModel = apps.get_model("myapp", "MyModel")
+
+            MyModel.many_to_many.through.objects.filter(id=1).first()
+
+        issues = MigrationLinter.get_runpython_model_import_issues(forward_method)
+        self.assertEqual(0, len(issues))
+
+    def test_missing_m2m_through_orm(self):
+        def forward_method(apps, schema_editor):
+            from tests.test_project.app_data_migrations.models import MyModel
+
+            MyModel.many_to_many.through.objects.filter(id=1).first()
+
+        issues = MigrationLinter.get_runpython_model_import_issues(forward_method)
+        self.assertEqual(1, len(issues))
+
 
 class DataMigrationModelVariableNamingTestCase(unittest.TestCase):
     def test_same_variable_name(self):