Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow any reverse relation using ForeignObjectRel to be type checked #1451

Merged
merged 4 commits into from
Apr 24, 2023

Conversation

Photonios
Copy link
Contributor

@Photonios Photonios commented Apr 21, 2023

Currently, reverse relations created using just ForeignObjectRel aren't checked. A real-world example of this is django-composite-foreignkey. Reverse relations created by it aren't discovered by the mypy plugin.

If anyone has any ideas on how to write a proper test for this, let me know.

Currently, reverse relations created using just `ForeignObjectRel`
aren't checked. A real-world example of this is `django-composite-foreignkey`.
Reverse relations created by it aren't discovered by the mypy plugin.
@intgr
Copy link
Collaborator

intgr commented Apr 21, 2023

Whoa there. You need to take a few steps back and explain what ForeignObjectRel does, how one would use it and what django-stubs should do with it. If possible, with vanilla Django, not an external library like django-composite-foreignkey.

As far as I can tell, ForeignObjectRel isn't documented in Django, apart from passing mentions in changelogs. Is it considered a stable API in Django? https://www.google.com/search?q=%22ForeignObjectRel%22+site%3Adocs.djangoproject.com

@sobolevn
Copy link
Member

Yes, adding a test case with pure django objects would help a lot as well.

@Photonios
Copy link
Contributor Author

Whoa there. You need to take a few steps back and explain what ForeignObjectRel does, how one would use it and what django-stubs should do with it. If possible, with vanilla Django, not an external library like django-composite-foreignkey.

ForeignObject and the related ForeignObject are the base classes for ForeignKey, ManyToOneRel and ManyToManyRel.

Although ForeignKey does not have support for creating multi-column relationships, the underlying ForeignObject does support this. I've demonstrated this in the test I added in my last commit.

Django does not truly support composite/multi-column foreign keys as support for it hasn't yet been added to the schema editor and migration subsystem. ForeignObject is frequently used to deal databases modeled/managed outside Django. Although one cannot manage the actual database constraint with Django, the relationship can be properly expressed in Django with ForeignObjec.

As far as I can tell, ForeignObjectRel isn't documented in Django, apart from passing mentions in changelogs. Is it considered a stable API in Django? https://www.google.com/search?q=%22ForeignObjectRel%22+site%3Adocs.djangoproject.com

I don't think it's considered a truly stable/documented API, but the fact that changes to are mentioned in the changelogs indicates that the Django developers are aware there are quite a few third-party packages out there relying on it. Making it unlikely to disappear suddenly or subject to intense changes.

Some more googling reveals that ForeignObject and related classes are generally considered building blocks for users to build their own relationship abstractions. Examples:

On top of that, the class has existed since Django 1.6, introduced here: django/django@266de5f

All-in; I think it's worth supporting. It would allow everyone using ForeignObject directly or one of the third-party packages to support multi-column foreign keys to have their code type-checked correctly.


The test I added demonstrates how to use ForeignObject directly to create a virtual multi-column foreign key. The test fails against master.

Copy link
Collaborator

@intgr intgr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation, makes lots of sense to me.

main: |
from app1.models import Model1, Model2

reveal_type(Model2.model_1) # N: Revealed type is "django.db.models.fields.related.ForeignObject[Any, Any]"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you also add a check for Model2().model_1, does it correctly show up as Model1 type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it gets typed as Any. I will have a look to see if I can fix this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a fix and updated the tests. It type-checks correctly now.

@Photonios Photonios force-pushed the support-composite-foreign-key branch from 1048b5a to 77fe2cf Compare April 24, 2023 07:52
@Photonios Photonios requested a review from intgr April 24, 2023 07:55
Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you!

Comment on lines 34 to 39
RELATED_FIELDS_CLASSES = {
FOREIGN_OBJECT_FULLNAME,
FOREIGN_KEY_FULLNAME,
ONETOONE_FIELD_FULLNAME,
MANYTOMANY_FIELD_FULLNAME,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RELATED_FIELDS_CLASSES = {
FOREIGN_OBJECT_FULLNAME,
FOREIGN_KEY_FULLNAME,
ONETOONE_FIELD_FULLNAME,
MANYTOMANY_FIELD_FULLNAME,
}
RELATED_FIELDS_CLASSES = frozenset((
FOREIGN_OBJECT_FULLNAME,
FOREIGN_KEY_FULLNAME,
ONETOONE_FIELD_FULLNAME,
MANYTOMANY_FIELD_FULLNAME,
))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've applied the suggestion.

@sobolevn sobolevn merged commit 2969651 into typeddjango:master Apr 24, 2023
(
FOREIGN_OBJECT_FULLNAME,
FOREIGN_KEY_FULLNAME,
ONETOONE_FIELD_FULLNAME,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both ForiegnKey and OneToOneField are subclasses of ForeignObject, so these could be removed now.

ManyToManyField has a separate inheritance hierarchy though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this will additionally apply to GenericRelation, which is another subclass of ForeignObject. Seems like a good thing, but ideally we should add a test to make sure it works as expected.

@Photonios Photonios deleted the support-composite-foreign-key branch April 24, 2023 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants