Repository: typeddjango/django-stubs

Issue:
Sitemap type anotation is too restrictive
# Bug report

<!--
Hi, thanks for submitting a bug. We appreciate that.

But, we will need some information about what's wrong to help you.
-->

## What's wrong

I'm having a similar issue to #1000 when type annotating django sitemaps.

The following is not passing:

```python
class OfferSitemap(Sitemap):
    priority = 1
    changefreq = "always"

    def items(self) -> QuerySet[Offer]:
        return Offer.objects.all()

    def location(self, item: Offer) -> str:
        return reverse(
            "myapp:detail-offer",
            kwargs={
                "provider_name": item.provider.url_name,
                "offer_name": item.url_name,
            },
        )

# error: Argument 1 of "location" is incompatible with supertype "Sitemap"; supertype defines the argument type as "Model"
# note: This violates the Liskov substitution principle
```

## How is that should be

From the docs: 

> [items()](https://docs.djangoproject.com/en/4.1/ref/contrib/sitemaps/#django.contrib.sitemaps.Sitemap.items) is a method that returns a [sequence](https://docs.python.org/3/glossary.html#term-sequence) or QuerySet of objects. The objects returned will get passed to any callable methods corresponding to a sitemap property ([location](https://docs.djangoproject.com/en/4.1/ref/contrib/sitemaps/#django.contrib.sitemaps.Sitemap.location), [lastmod](https://docs.djangoproject.com/en/4.1/ref/contrib/sitemaps/#django.contrib.sitemaps.Sitemap.lastmod), [changefreq](https://docs.djangoproject.com/en/4.1/ref/contrib/sitemaps/#django.contrib.sitemaps.Sitemap.changefreq), and [priority](https://docs.djangoproject.com/en/4.1/ref/contrib/sitemaps/#django.contrib.sitemaps.Sitemap.priority)).

I think the type could be more generic because the `items` method is the one that determines the type of the `item` variable every other methods are using. Maybe taking a similar approach to #800 with an `_Item` Typevar ?

Let me know if you think this is a correct approach and I'll be happy to submit a PR to address that.

Cheers

## System information

- OS: **ubuntu20.04**
- `python` version: **3.8**
- `django` version: **3.2.15**
- `mypy` version: **0.950**
- `django-stubs` version: **1.11.0**
- `django-stubs-ext` version: **0.5.0**



Discussion:
Using a `TypeVar` seems correct to me, please do submit a PR. That should fix #1000 as well.

Resolving patch:
diff --git a/django_stubs_ext/django_stubs_ext/patch.py b/django_stubs_ext/django_stubs_ext/patch.py
--- a/django_stubs_ext/django_stubs_ext/patch.py
+++ b/django_stubs_ext/django_stubs_ext/patch.py
@@ -4,6 +4,7 @@
 from django import VERSION as VERSION
 from django.contrib.admin import ModelAdmin
 from django.contrib.admin.options import BaseModelAdmin
+from django.contrib.sitemaps import Sitemap
 from django.contrib.syndication.views import Feed
 from django.core.files.utils import FileProxyMixin
 from django.core.paginator import Paginator
@@ -60,6 +61,7 @@ def __repr__(self) -> str:
     MPGeneric(BaseModelForm),
     MPGeneric(BaseModelFormSet),
     MPGeneric(Feed),
+    MPGeneric(Sitemap),
     MPGeneric(FileProxyMixin),
     MPGeneric(Lookup),
     # These types do have native `__class_getitem__` method since django 3.1:


Test patch:
diff --git a/tests/typecheck/contrib/sitemaps/test_generic_sitemap.yml b/tests/typecheck/contrib/sitemaps/test_generic_sitemap.yml
new file mode 100644
--- /dev/null
+++ b/tests/typecheck/contrib/sitemaps/test_generic_sitemap.yml
@@ -0,0 +1,80 @@
+-   case: test_items_custom_model
+    main: |
+        from django.contrib.sitemaps import GenericSitemap, Sitemap
+        from django.contrib.sitemaps.views import sitemap
+        from django.db.models import QuerySet
+        from django.urls import path, reverse
+        from myapp.models import Offer
+
+        class OfferSitemap(Sitemap[Offer]):
+            priority = 1
+            changefreq = "always"
+
+            def items(self) -> QuerySet[Offer]:
+                return Offer.objects.all()
+
+            def location(self, item: Offer) -> str:
+                return reverse(
+                    "myapp:detail-offer",
+                    kwargs={
+                        "provider_name": item.provider,
+                        "offer_name": item.trc, # E: "Offer" has no attribute "trc"
+                    },
+                )
+
+        class WrongOfferSitemap(Sitemap[Offer]):
+            def items(self) -> str:
+                return "Yes"
+            def location(self, item: str) -> int:
+                return 1
+
+        info_dict = {"queryset": Offer.objects.all()}
+        broken_info_dict = {"queryset": [1, 2]}
+
+        urlpatterns = [
+            path(
+                'sitemap.xml', sitemap,
+                {'sitemaps': {'offers': GenericSitemap[Offer](info_dict, priority=0.6)}},
+                name='django.contrib.sitemaps.views.sitemap'
+            ),
+            path(
+                'broken_sitemap.xml', sitemap,
+                {'sitemaps': {'offers': GenericSitemap[Offer](broken_info_dict, priority=0.6)}},
+                name='django.contrib.sitemaps.views.broken_sitemap'
+            ),
+        ]
+    out: |
+        main:24: error: Return type "str" of "items" incompatible with return type "Iterable[Offer]" in supertype "Sitemap"
+        main:26: error: Argument 1 of "location" is incompatible with supertype "Sitemap"; supertype defines the argument type as "Offer"
+        main:26: note: This violates the Liskov substitution principle
+        main:26: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
+        main:26: error: Return type "int" of "location" incompatible with return type "str" in supertype "Sitemap"
+        main:40: error: Argument 1 to "GenericSitemap" has incompatible type "Dict[str, List[int]]"; expected "Mapping[str, Union[datetime, _QuerySet[Offer, Offer], str]]"
+
+    installed_apps:
+        - myapp
+    files:
+        -   path: myapp/__init__.py
+        -   path: myapp/models.py
+            content: |
+                from django.db import models
+
+                class Offer(models.Model):
+                    provider = models.ForeignKey("self", on_delete=models.CASCADE)
+                    url_name = models.CharField()
+
+-   case: test_items_string_sequence
+    main: |
+        from django.contrib.sitemaps import Sitemap
+        from typing import List
+        from django.urls import reverse
+
+        class StaticViewSitemap(Sitemap[str]):
+            priority = 1
+            changefreq = "always"
+
+            def items(self) -> List[str]:
+                return ["home", "about", "contact", "recommendations", "privacy-policy", "blog"]
+
+            def location(self, item: str) -> str:
+                return reverse(item)