Repository: 3YOURMIND/django-migration-linter

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.
```


Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/222?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#222](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/222?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (2114d61) into [main](https://codecov.io/gh/3YOURMIND/django-migration-linter/commit/3baf9487bde6ae27c3ba7623a410ab6c39bb0584?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3baf948) will **decrease** coverage by `0.17%`.
> The diff coverage is `71.42%`.

```diff
@@            Coverage Diff             @@
##             main     #222      +/-   ##
==========================================
- Coverage   93.60%   93.43%   -0.18%     
==========================================
  Files          79       79              
  Lines        1815     1828      +13     
==========================================
+ Hits         1699     1708       +9     
- Misses        116      120       +4     
```


| [Impacted Files](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/222?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [tests/functional/test\_data\_migrations.py](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/222/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dGVzdHMvZnVuY3Rpb25hbC90ZXN0X2RhdGFfbWlncmF0aW9ucy5weQ==) | `78.65% <66.66%> (-0.87%)` | :arrow_down: |
| [django\_migration\_linter/migration\_linter.py](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/222/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZGphbmdvX21pZ3JhdGlvbl9saW50ZXIvbWlncmF0aW9uX2xpbnRlci5weQ==) | `84.75% <100.00%> (+0.04%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/222?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/222?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [3baf948...2114d61](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/222?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


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):