mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-08 04:54:48 +08:00
Specific return types for values and values list (#53)
* Instead of using Literal types, overload QuerySet.values_list in the plugin. Fixes #43. - Add a couple of extra type checks that Django makes: 1) 'flat' and 'named' can't be used together. 2) 'flat' is not valid when values_list is called with more than one field. * Determine better row types for values_list/values based on fields specified. - In the case of values_list, we use a Row type with either a single primitive, Tuple, or NamedTuple. - In the case of values, we use a TypedDict. - In both cases, Any is used as a fallback for individual fields if those fields cannot be resolved. A couple other fixes I made along the way: - Don't create reverse relation for ForeignKeys with related_name='+' - Don't skip creating other related managers in AddRelatedManagers if a dynamic value is encountered for related_name parameter, or if the type cannot be determined. * Fix for TypedDict so that they are considered anonymous. * Clean up some comments. * Implement making TypedDict anonymous in a way that doesn't crash sometimes. * Fix flake8 errors. * Remove even uglier hack about making TypedDict anonymous. * Address review comments. Write a few better comments inside tests. * Fix crash when running with mypyc ("interpreted classes cannot inherit from compiled") due to the way I extended TypedDictType. - Implemented the hack in another way that works on mypyc. - Added a couple extra tests of accessing 'id' / 'pk' via values_list. * Fix flake8 errors. * Support annotation expressions (use type Any) for TypedDicts row types returned by values_list. - Bonus points: handle values_list gracefully (use type Any) where Tuples are returned where some of the fields arguments are not string literals.
This commit is contained in:
committed by
Maxim Kurnikov
parent
5c6be7ad12
commit
5b455b729a
@@ -9,13 +9,15 @@ class Blog(models.Model):
|
||||
class BlogQuerySet(models.QuerySet[Blog]):
|
||||
pass
|
||||
|
||||
class Entry(models.Model):
|
||||
blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name="entries")
|
||||
title = models.CharField(max_length=100)
|
||||
|
||||
|
||||
# Test that second type argument gets filled automatically
|
||||
blog_qs: models.QuerySet[Blog]
|
||||
reveal_type(blog_qs) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog, main.Blog]'
|
||||
|
||||
Blog.objects.values_list('id', flat=True, named=True) # E: 'flat' and 'named' can't be used together.
|
||||
Blog.objects.values_list('id', 'extra_arg', flat=True) # E: 'flat' is not valid when values_list is called with more than one field.
|
||||
|
||||
reveal_type(Blog.objects.in_bulk([1])) # E: Revealed type is 'builtins.dict[Any, main.Blog*]'
|
||||
reveal_type(Blog.objects.in_bulk()) # E: Revealed type is 'builtins.dict[Any, main.Blog*]'
|
||||
reveal_type(Blog.objects.in_bulk(['beatles_blog'], field_name='name')) # E: Revealed type is 'builtins.dict[Any, main.Blog*]'
|
||||
@@ -23,9 +25,9 @@ reveal_type(Blog.objects.in_bulk(['beatles_blog'], field_name='name')) # E: Reve
|
||||
# When ANDing QuerySets, the left-side's _Row parameter is used
|
||||
reveal_type(Blog.objects.all() & Blog.objects.values()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, main.Blog*]'
|
||||
reveal_type(Blog.objects.values() & Blog.objects.values()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.dict*[builtins.str, Any]]'
|
||||
reveal_type(Blog.objects.values_list('id', 'name') & Blog.objects.values()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.tuple*[Any]]'
|
||||
reveal_type(Blog.objects.values_list('id', 'name', named=True) & Blog.objects.values()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, typing.NamedTuple*]'
|
||||
reveal_type(Blog.objects.values_list('id', flat=True) & Blog.objects.values()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Any]'
|
||||
reveal_type(Blog.objects.values_list('id', 'name') & Blog.objects.values()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str]]'
|
||||
reveal_type(Blog.objects.values_list('id', 'name', named=True) & Blog.objects.values()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str, fallback=main.Row]]'
|
||||
reveal_type(Blog.objects.values_list('id', flat=True) & Blog.objects.values()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.int*]'
|
||||
|
||||
# .dates / .datetimes
|
||||
reveal_type(Blog.objects.dates("created_at", "day")) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, datetime.date]'
|
||||
@@ -57,41 +59,203 @@ reveal_type(values_qs.in_bulk()) # E: Revealed type is 'builtins.dict[Any, main.
|
||||
|
||||
|
||||
values_list_qs = Blog.objects.values_list('id', 'name')
|
||||
reveal_type(values_list_qs) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.tuple[Any]]'
|
||||
reveal_type(values_list_qs.all()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.tuple*[Any]]'
|
||||
reveal_type(values_list_qs.get(id=1)) # E: Revealed type is 'builtins.tuple*[Any]'
|
||||
reveal_type(iter(values_list_qs)) # E: Revealed type is 'typing.Iterator[builtins.tuple*[Any]]'
|
||||
reveal_type(values_list_qs.iterator()) # E: Revealed type is 'typing.Iterator[builtins.tuple*[Any]]'
|
||||
reveal_type(values_list_qs.first()) # E: Revealed type is 'Union[builtins.tuple*[Any], None]'
|
||||
reveal_type(values_list_qs.earliest()) # E: Revealed type is 'builtins.tuple*[Any]'
|
||||
reveal_type(values_list_qs[0]) # E: Revealed type is 'builtins.tuple*[Any]'
|
||||
reveal_type(values_list_qs[:9]) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.tuple*[Any]]'
|
||||
reveal_type(values_list_qs) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str]]'
|
||||
reveal_type(values_list_qs.all()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str]]'
|
||||
reveal_type(values_list_qs.get(id=1)) # E: Revealed type is 'Tuple[builtins.int, builtins.str]'
|
||||
reveal_type(iter(values_list_qs)) # E: Revealed type is 'typing.Iterator[Tuple[builtins.int, builtins.str]]'
|
||||
reveal_type(values_list_qs.iterator()) # E: Revealed type is 'typing.Iterator[Tuple[builtins.int, builtins.str]]'
|
||||
reveal_type(values_list_qs.first()) # E: Revealed type is 'Union[Tuple[builtins.int, builtins.str], None]'
|
||||
reveal_type(values_list_qs.earliest()) # E: Revealed type is 'Tuple[builtins.int, builtins.str]'
|
||||
reveal_type(values_list_qs[0]) # E: Revealed type is 'Tuple[builtins.int, builtins.str]'
|
||||
reveal_type(values_list_qs[:9]) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str]]'
|
||||
reveal_type(values_list_qs.in_bulk()) # E: Revealed type is 'builtins.dict[Any, main.Blog*]'
|
||||
|
||||
|
||||
flat_values_list_qs = Blog.objects.values_list('id', flat=True)
|
||||
reveal_type(flat_values_list_qs) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Any]'
|
||||
reveal_type(flat_values_list_qs.all()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Any]'
|
||||
reveal_type(flat_values_list_qs.get(id=1)) # E: Revealed type is 'Any'
|
||||
reveal_type(iter(flat_values_list_qs)) # E: Revealed type is 'typing.Iterator[Any]'
|
||||
reveal_type(flat_values_list_qs.iterator()) # E: Revealed type is 'typing.Iterator[Any]'
|
||||
reveal_type(flat_values_list_qs.first()) # E: Revealed type is 'Union[Any, None]'
|
||||
reveal_type(flat_values_list_qs.earliest()) # E: Revealed type is 'Any'
|
||||
reveal_type(flat_values_list_qs[0]) # E: Revealed type is 'Any'
|
||||
reveal_type(flat_values_list_qs[:9]) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Any]'
|
||||
reveal_type(flat_values_list_qs) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.int]'
|
||||
reveal_type(flat_values_list_qs.all()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.int*]'
|
||||
reveal_type(flat_values_list_qs.get(id=1)) # E: Revealed type is 'builtins.int*'
|
||||
reveal_type(iter(flat_values_list_qs)) # E: Revealed type is 'typing.Iterator[builtins.int*]'
|
||||
reveal_type(flat_values_list_qs.iterator()) # E: Revealed type is 'typing.Iterator[builtins.int*]'
|
||||
reveal_type(flat_values_list_qs.first()) # E: Revealed type is 'Union[builtins.int*, None]'
|
||||
reveal_type(flat_values_list_qs.earliest()) # E: Revealed type is 'builtins.int*'
|
||||
reveal_type(flat_values_list_qs[0]) # E: Revealed type is 'builtins.int*'
|
||||
reveal_type(flat_values_list_qs[:9]) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.int*]'
|
||||
reveal_type(flat_values_list_qs.in_bulk()) # E: Revealed type is 'builtins.dict[Any, main.Blog*]'
|
||||
|
||||
|
||||
named_values_list_qs = Blog.objects.values_list('id', named=True)
|
||||
reveal_type(named_values_list_qs) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, typing.NamedTuple]'
|
||||
reveal_type(named_values_list_qs.all()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, typing.NamedTuple*]'
|
||||
reveal_type(named_values_list_qs.get(id=1)) # E: Revealed type is 'typing.NamedTuple*'
|
||||
reveal_type(iter(named_values_list_qs)) # E: Revealed type is 'typing.Iterator[typing.NamedTuple*]'
|
||||
reveal_type(named_values_list_qs.iterator()) # E: Revealed type is 'typing.Iterator[typing.NamedTuple*]'
|
||||
reveal_type(named_values_list_qs.first()) # E: Revealed type is 'Union[typing.NamedTuple*, None]'
|
||||
reveal_type(named_values_list_qs.earliest()) # E: Revealed type is 'typing.NamedTuple*'
|
||||
reveal_type(named_values_list_qs[0]) # E: Revealed type is 'typing.NamedTuple*'
|
||||
reveal_type(named_values_list_qs[:9]) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, typing.NamedTuple*]'
|
||||
reveal_type(named_values_list_qs) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, fallback=main.Row1]]'
|
||||
reveal_type(named_values_list_qs.all()) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, fallback=main.Row1]]'
|
||||
reveal_type(named_values_list_qs.get(id=1)) # E: Revealed type is 'Tuple[builtins.int, fallback=main.Row1]'
|
||||
reveal_type(iter(named_values_list_qs)) # E: Revealed type is 'typing.Iterator[Tuple[builtins.int, fallback=main.Row1]]'
|
||||
reveal_type(named_values_list_qs.iterator()) # E: Revealed type is 'typing.Iterator[Tuple[builtins.int, fallback=main.Row1]]'
|
||||
reveal_type(named_values_list_qs.first()) # E: Revealed type is 'Union[Tuple[builtins.int, fallback=main.Row1], None]'
|
||||
reveal_type(named_values_list_qs.earliest()) # E: Revealed type is 'Tuple[builtins.int, fallback=main.Row1]'
|
||||
reveal_type(named_values_list_qs[0]) # E: Revealed type is 'Tuple[builtins.int, fallback=main.Row1]'
|
||||
reveal_type(named_values_list_qs[:9]) # E: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, fallback=main.Row1]]'
|
||||
reveal_type(named_values_list_qs.in_bulk()) # E: Revealed type is 'builtins.dict[Any, main.Blog*]'
|
||||
|
||||
[out]
|
||||
|
||||
[CASE test_queryset_values_list_custom_primary_key]
|
||||
from django.db import models
|
||||
|
||||
class Blog(models.Model):
|
||||
primary_uuid = models.UUIDField(primary_key=True)
|
||||
|
||||
class Entry(models.Model):
|
||||
blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name="entries")
|
||||
|
||||
# Blog has a primary key field specified, so no automatic 'id' field is expected to exist
|
||||
reveal_type(Blog.objects.values_list('id', flat=True).get()) # E: Revealed type is 'Any'
|
||||
|
||||
# Access Blog's pk (which is UUID field)
|
||||
reveal_type(Blog.objects.values_list('pk', flat=True).get()) # E: Revealed type is 'uuid.UUID*'
|
||||
|
||||
# Accessing PK of model pointed to by foreign key
|
||||
reveal_type(Entry.objects.values_list('blog', flat=True).get()) # E: Revealed type is 'uuid.UUID*'
|
||||
# Alternative way of accessing PK of model pointed to by foreign key
|
||||
reveal_type(Entry.objects.values_list('blog_id', flat=True).get()) # E: Revealed type is 'uuid.UUID*'
|
||||
# Yet another (more explicit) way of accessing PK of related model
|
||||
reveal_type(Entry.objects.values_list('blog__pk', flat=True).get()) # E: Revealed type is 'uuid.UUID*'
|
||||
|
||||
# Blog has a primary key field specified, so no automatic 'id' field is expected to exist
|
||||
reveal_type(Entry.objects.values_list('blog__id', flat=True).get()) # E: Revealed type is 'Any'
|
||||
|
||||
[CASE test_queryset_values_list]
|
||||
from django.db import models
|
||||
|
||||
class Blog(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
created_at = models.DateTimeField()
|
||||
|
||||
class Entry(models.Model):
|
||||
blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name="entries")
|
||||
nullable_blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name="+", null=True)
|
||||
blog_with_related_query_name = models.ForeignKey(Blog, on_delete=models.CASCADE, related_query_name="my_related_query_name")
|
||||
title = models.CharField(max_length=100)
|
||||
|
||||
class BlogChild(Blog):
|
||||
child_field = models.CharField(max_length=100)
|
||||
|
||||
# Emulate at type-check time the errors that Django reports
|
||||
Blog.objects.values_list('id', flat=True, named=True) # E: 'flat' and 'named' can't be used together.
|
||||
Blog.objects.values_list('id', 'created_at', flat=True) # E: 'flat' is not valid when values_list is called with more than one field.
|
||||
|
||||
# values_list where parameter types are all known
|
||||
reveal_type(Blog.objects.values_list('id', 'created_at').get()) # E: Revealed type is 'Tuple[builtins.int, datetime.datetime]'
|
||||
tup = Blog.objects.values_list('id', 'created_at').get()
|
||||
reveal_type(tup[0]) # E: Revealed type is 'builtins.int'
|
||||
reveal_type(tup[1]) # E: Revealed type is 'datetime.datetime'
|
||||
tup[2] # E: Tuple index out of range
|
||||
|
||||
# values_list returning namedtuple
|
||||
reveal_type(Blog.objects.values_list('id', 'created_at', named=True).get()) # E: Revealed type is 'Tuple[builtins.int, datetime.datetime, fallback=main.Row]'
|
||||
|
||||
# Invalid lookups produce Any type rather than giving errors.
|
||||
reveal_type(Blog.objects.values_list('id', 'invalid_lookup').get()) # E: Revealed type is 'Tuple[builtins.int, Any]'
|
||||
reveal_type(Blog.objects.values_list('entries_id', flat=True).get()) # E: Revealed type is 'Any'
|
||||
reveal_type(Blog.objects.values_list('entries__foo', flat=True).get()) # E: Revealed type is 'Any'
|
||||
reveal_type(Blog.objects.values_list('+', flat=True).get()) # E: Revealed type is 'Any'
|
||||
|
||||
# Foreign key
|
||||
reveal_type(Entry.objects.values_list('blog', flat=True).get()) # E: Revealed type is 'builtins.int*'
|
||||
reveal_type(Entry.objects.values_list('blog__id', flat=True).get()) # E: Revealed type is 'builtins.int*'
|
||||
reveal_type(Entry.objects.values_list('blog__pk', flat=True).get()) # E: Revealed type is 'builtins.int*'
|
||||
reveal_type(Entry.objects.values_list('blog_id', flat=True).get()) # E: Revealed type is 'builtins.int*'
|
||||
|
||||
# Foreign key (nullable=True)
|
||||
reveal_type(Entry.objects.values_list('nullable_blog', flat=True).get()) # E: Revealed type is 'Union[builtins.int, None]'
|
||||
reveal_type(Entry.objects.values_list('nullable_blog_id', flat=True).get()) # E: Revealed type is 'Union[builtins.int, None]'
|
||||
reveal_type(Entry.objects.values_list('nullable_blog__id', flat=True).get()) # E: Revealed type is 'Union[builtins.int, None]'
|
||||
reveal_type(Entry.objects.values_list('nullable_blog__pk', flat=True).get()) # E: Revealed type is 'Union[builtins.int, None]'
|
||||
|
||||
# Reverse relation of ForeignKey
|
||||
reveal_type(Blog.objects.values_list('entries', flat=True).get()) # E: Revealed type is 'Union[builtins.int, None]'
|
||||
reveal_type(Blog.objects.values_list('entries__id', flat=True).get()) # E: Revealed type is 'Union[builtins.int, None]'
|
||||
reveal_type(Blog.objects.values_list('entries__title', flat=True).get()) # E: Revealed type is 'Union[builtins.str, None]'
|
||||
|
||||
# Reverse relation of ForeignKey (with related_query_name set)
|
||||
reveal_type(Blog.objects.values_list('my_related_query_name__id', flat=True).get()) # E: Revealed type is 'Union[builtins.int, None]'
|
||||
|
||||
# Basic inheritance
|
||||
reveal_type(BlogChild.objects.values_list('id', 'created_at', 'child_field').get()) # E: Revealed type is 'Tuple[builtins.int, datetime.datetime, builtins.str]'
|
||||
|
||||
|
||||
|
||||
[CASE test_queryset_values_list_and_values_behavior_with_no_fields_specified_and_accessing_unknown_attributes]
|
||||
from django.db import models
|
||||
|
||||
class Blog(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
created_at = models.DateTimeField()
|
||||
|
||||
row_named = Blog.objects.values_list('id', 'created_at', named=True).get()
|
||||
reveal_type(row_named.id) # E: Revealed type is 'builtins.int'
|
||||
reveal_type(row_named.created_at) # E: Revealed type is 'datetime.datetime'
|
||||
row_named.non_existent_field # E: "Row" has no attribute "non_existent_field"
|
||||
|
||||
|
||||
# When no fields are specified, fallback to Any
|
||||
row_named_no_fields = Blog.objects.values_list(named=True).get()
|
||||
reveal_type(row_named_no_fields) # E: Revealed type is 'Tuple[, fallback=django._NamedTupleAnyAttr]'
|
||||
|
||||
# Don't complain about access to any attribute for now
|
||||
reveal_type(row_named_no_fields.non_existent_field) # E: Revealed type is 'Any'
|
||||
row_named_no_fields.non_existent_field = 1
|
||||
|
||||
# It should still behave like a NamedTuple
|
||||
reveal_type(row_named_no_fields._asdict()) # E: Revealed type is 'builtins.dict[builtins.str, Any]'
|
||||
|
||||
|
||||
dict_row = Blog.objects.values('id', 'created_at').get()
|
||||
reveal_type(dict_row["id"]) # E: Revealed type is 'builtins.int'
|
||||
reveal_type(dict_row["created_at"]) # E: Revealed type is 'datetime.datetime'
|
||||
dict_row["non_existent_field"] # E: 'non_existent_field' is not a valid TypedDict key; expected one of ('id', 'created_at')
|
||||
dict_row.pop('created_at')
|
||||
dict_row.pop('non_existent_field') # E: 'non_existent_field' is not a valid TypedDict key; expected one of ('id', 'created_at')
|
||||
|
||||
row_dict_no_fields = Blog.objects.values().get()
|
||||
reveal_type(row_dict_no_fields) # E: Revealed type is 'builtins.dict*[builtins.str, Any]'
|
||||
reveal_type(row_dict_no_fields["non_existent_field"]) # E: Revealed type is 'Any'
|
||||
|
||||
[CASE values_with_annotate_inside_the_expressions]
|
||||
from django.db import models
|
||||
from django.db.models.functions import Lower, Upper
|
||||
|
||||
class Publisher(models.Model):
|
||||
pass
|
||||
|
||||
class Book(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, related_name='books')
|
||||
|
||||
reveal_type(Publisher().books.values('name', lower_name=Lower('name'), upper_name=Upper('name'))) # E: Revealed type is 'django.db.models.query.QuerySet[main.Book*, TypedDict({'name'?: builtins.str, 'lower_name'?: Any, 'upper_name'?: Any})]'
|
||||
|
||||
|
||||
[CASE values_and_values_list_some_dynamic_fields]
|
||||
from django.db import models
|
||||
|
||||
class Publisher(models.Model):
|
||||
pass
|
||||
|
||||
class Book(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, related_name='books')
|
||||
|
||||
some_dynamic_field = 'publisher'
|
||||
|
||||
# Correct Tuple field types should be filled in when string literal is used, while Any is used for dynamic fields
|
||||
reveal_type(Publisher().books.values_list('name', some_dynamic_field)) # E: Revealed type is 'django.db.models.query.QuerySet[main.Book*, Tuple[builtins.str, Any]]'
|
||||
|
||||
# Flat with dynamic fields (there is only 1), means of course Any
|
||||
reveal_type(Publisher().books.values_list(some_dynamic_field, flat=True)) # E: Revealed type is 'django.db.models.query.QuerySet[main.Book*, Any]'
|
||||
|
||||
# A NamedTuple with a fallback to Any could be implemented, but for now that's unsupported, so all
|
||||
# fields on the NamedTuple are Any for now
|
||||
reveal_type(Publisher().books.values_list('name', some_dynamic_field, named=True).name) # E: Revealed type is 'Any'
|
||||
|
||||
# A TypedDict with a fallback to Any could be implemented, but for now that's unsupported,
|
||||
# so an ordinary Dict is used for now.
|
||||
reveal_type(Publisher().books.values(some_dynamic_field, 'name')) # E: Revealed type is 'django.db.models.query.QuerySet[main.Book*, builtins.dict[builtins.str, Any]]'
|
||||
|
||||
@@ -287,3 +287,23 @@ class Publisher(models.Model):
|
||||
pass
|
||||
reveal_type(Book().publisher) # E: Revealed type is 'main.Publisher*'
|
||||
[out]
|
||||
|
||||
[CASE test_foreign_key_field_without_backwards_relation]
|
||||
from django.db import models
|
||||
|
||||
class Publisher(models.Model):
|
||||
pass
|
||||
|
||||
class Book(models.Model):
|
||||
publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE,
|
||||
related_name='+')
|
||||
publisher2 = models.ForeignKey(to=Publisher, on_delete=models.CASCADE,
|
||||
related_name='books2')
|
||||
|
||||
book = Book()
|
||||
reveal_type(book.publisher) # E: Revealed type is 'main.Publisher*'
|
||||
|
||||
publisher = Publisher()
|
||||
reveal_type(publisher.books) # E: Revealed type is 'Any'
|
||||
reveal_type(publisher.books2) # E: Revealed type is 'django.db.models.manager.RelatedManager[main.Book]'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user