Repository: typeddjango/django-stubs

Issue:
Plugin regression with mypy 0.99x: FieldDoesNotExist
`mypy_django_plugin` causes a mypy internal error when running `typecheck_tests.py` on Django 3.2 source with mypy 0.991. Does not occur with Django 4.0.

See `typecheck` failures in PR #1260.

I have not yet had the time to investigate this. Any help would be welcome.

```
  File "/home/runner/work/django-stubs/django-stubs/mypy_django_plugin/transformers/fields.py", line 117, in set_descriptor_types_for_field_callback
  File "/home/runner/work/django-stubs/django-stubs/mypy_django_plugin/transformers/fields.py", line 215, in transform_into_proper_return_type
  File "/home/runner/work/django-stubs/django-stubs/mypy_django_plugin/transformers/fields.py", line 41, in _get_current_field_from_assignment
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/django/db/models/options.py", line 610, in get_field
  File "mypy/checkexpr.py", line 1004, in apply_function_plugin
  File "mypy/checkexpr.py", line 1181, in check_call_expr_with_callee_type
  File "mypy/checkexpr.py", line 1264, in check_call
  File "mypy/checkexpr.py", line 1466, in check_callable_call
  File "mypy/checkexpr.py", line 410, in visit_call_expr
  File "mypy/checkexpr.py", line 4665, in accept
  File "mypy/checkexpr.py", line 530, in visit_call_expr_inner
  File "mypy/nodes.py", line 1841, in accept
/home/runner/work/django-stubs/django-stubs/django-source/tests/model_fields/models.py:176: error: INTERNAL ERROR -- Please try using mypy master on GitHub:
FieldDoesNotExist: RenamedField has no field named 'modelname'
```



Discussion:
Please let me know if this issue affects any real codebase as well. Currently it has only been observed in our CI.

Possible work-around: downgrade to mypy 0.982.
From my digging:

`RenamedField` is [declared in Django's test suite](https://github.com/django/django/blob/4c85beca9d8bf24573d741e26ea36295c69af5b2/tests/model_fields/models.py#L175-L176) like:

```python
class RenamedField(models.Model):
    modelname = models.IntegerField(name="fieldname", choices=((1, 'One'),))
```

It should be noted that `name=` isn't a documented argument/feature and it seems that it renames the instance(?) attribute on the model. As such, you'd access the field value like: `RenamedField().fieldname`. If you do `RenamedField().modelname` you get a `FieldDoesNotExist` in your face.

I'm only guessing, but perhaps it could be useful to avoid shadowing? e.g.

```python
class X(models.Model):
    _type = models.IntegerField(name="type")
```

In any case, the plugin doesn't support the `name=` argument and we could try to catch `FieldDoesNotExist` around `.get_field(...)` here:

https://github.com/typeddjango/django-stubs/blob/7d50e5b1eaedc9e95dc8ed35f10f93ec73239ae0/mypy_django_plugin/transformers/fields.py#L34-L42


I'm suspecting that should be good enough until there would be any support for `name=` (if at all). Since we check

```python
if field_name is None:
    return None
```

a couple of lines before. As such, `field_name` is seen to appear on the class definition (if not None)

Resolving patch:
diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py
--- a/mypy_django_plugin/transformers/fields.py
+++ b/mypy_django_plugin/transformers/fields.py
@@ -1,5 +1,6 @@
 from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, cast
 
+from django.core.exceptions import FieldDoesNotExist
 from django.db.models.fields import AutoField, Field
 from django.db.models.fields.related import RelatedField
 from django.db.models.fields.reverse_related import ForeignObjectRel
@@ -38,8 +39,10 @@ def _get_current_field_from_assignment(
     if model_cls is None:
         return None
 
-    current_field = model_cls._meta.get_field(field_name)
-    return current_field
+    try:
+        return model_cls._meta.get_field(field_name)
+    except FieldDoesNotExist:
+        return None
 
 
 def reparametrize_related_field_type(related_field_type: Instance, set_type: MypyType, get_type: MypyType) -> Instance:


Test patch:
diff --git a/tests/typecheck/fields/test_base.yml b/tests/typecheck/fields/test_base.yml
--- a/tests/typecheck/fields/test_base.yml
+++ b/tests/typecheck/fields/test_base.yml
@@ -180,3 +180,24 @@
         obj = MyModel()
 
         reveal_type(obj.small) # N: Revealed type is "builtins.int"
+
+- case: test_ignores_renamed_field
+  main: |
+        # Ref: https://github.com/typeddjango/django-stubs/issues/1261
+        # Django modifies the model so it doesn't have 'modelname', but we don't follow
+        # along. But the 'name=' argument to a field isn't a documented feature.
+        from myapp.models import RenamedField
+        instance = RenamedField()
+        reveal_type(instance.modelname) # N: Revealed type is "builtins.int"
+        instance.fieldname # E: "RenamedField" has no attribute "fieldname"
+        instance.modelname = 1
+        instance.fieldname = 1 # E: "RenamedField" has no attribute "fieldname"
+  installed_apps:
+    - myapp
+  files:
+    - path: myapp/__init__.py
+    - path: myapp/models.py
+      content: |
+        from django.db import models
+        class RenamedField(models.Model):
+            modelname = models.IntegerField(name="fieldname", choices=((1, 'One'),))