Skip to content

Commit 4a5b065

Browse files
authored
Remove QuerySet alias hacks via PEP 696 TypeVar defaults (#2104)
The `QuerySet` class was previously named `_QuerySet` and had three aliases: `QuerySet`, `QuerySetAny` and `ValuesQuerySet`. These hacks were mainly needed to for the ergonomic single-parameter `QuerySet[Model]`, which expanded into `_QuerySet[Model, Model]` But now that mypy 1.10 implements PEP 696 to a fuller extent (Pyright also supports it), the 2nd type parameter can be a simple TypeVar that defaults to 1st type parameter.
1 parent b0858a7 commit 4a5b065

24 files changed

+124
-133
lines changed

README.md

-25
Original file line numberDiff line numberDiff line change
@@ -254,31 +254,6 @@ func(MyModel.objects.annotate(foo=Value("")).get(id=1)) # OK
254254
func(MyModel.objects.annotate(bar=Value("")).get(id=1)) # Error
255255
```
256256

257-
### How do I check if something is an instance of QuerySet in runtime?
258-
259-
A limitation of making `QuerySet` generic is that you can not use
260-
it for `isinstance` checks.
261-
262-
```python
263-
from django.db.models.query import QuerySet
264-
265-
def foo(obj: object) -> None:
266-
if isinstance(obj, QuerySet): # Error: Parameterized generics cannot be used with class or instance checks
267-
...
268-
```
269-
270-
To get around with this issue without making `QuerySet` non-generic,
271-
Django-stubs provides `django_stubs_ext.QuerySetAny`, a non-generic
272-
variant of `QuerySet` suitable for runtime type checking:
273-
274-
```python
275-
from django_stubs_ext import QuerySetAny
276-
277-
def foo(obj: object) -> None:
278-
if isinstance(obj, QuerySetAny): # OK
279-
...
280-
```
281-
282257
### Why am I getting incompatible argument type mentioning `_StrPromise`?
283258

284259
The lazy translation functions of Django (such as `gettext_lazy`) return a `Promise` instead of `str`. These two types [cannot be used interchangeably](https://github.com/typeddjango/django-stubs/pull/1139#issuecomment-1232167698). The return type of these functions was therefore [changed](https://github.com/typeddjango/django-stubs/pull/689) to reflect that.

django-stubs/db/models/manager.pyi

+4-8
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ from django.db.models.expressions import Combinable, OrderBy
77
from django.db.models.query import QuerySet, RawQuerySet
88
from typing_extensions import Self
99

10-
from django_stubs_ext import ValuesQuerySet
11-
1210
_T = TypeVar("_T", bound=Model, covariant=True)
1311

1412
class BaseManager(Generic[_T]):
@@ -107,15 +105,13 @@ class BaseManager(Generic[_T]):
107105
using: str | None = ...,
108106
) -> RawQuerySet: ...
109107
# The type of values may be overridden to be more specific in the mypy plugin, depending on the fields param
110-
def values(self, *fields: str | Combinable, **expressions: Any) -> ValuesQuerySet[_T, dict[str, Any]]: ...
108+
def values(self, *fields: str | Combinable, **expressions: Any) -> QuerySet[_T, dict[str, Any]]: ...
111109
# The type of values_list may be overridden to be more specific in the mypy plugin, depending on the fields param
112-
def values_list(
113-
self, *fields: str | Combinable, flat: bool = ..., named: bool = ...
114-
) -> ValuesQuerySet[_T, Any]: ...
115-
def dates(self, field_name: str, kind: str, order: str = ...) -> ValuesQuerySet[_T, datetime.date]: ...
110+
def values_list(self, *fields: str | Combinable, flat: bool = ..., named: bool = ...) -> QuerySet[_T, Any]: ...
111+
def dates(self, field_name: str, kind: str, order: str = ...) -> QuerySet[_T, datetime.date]: ...
116112
def datetimes(
117113
self, field_name: str, kind: str, order: str = ..., tzinfo: datetime.tzinfo | None = ...
118-
) -> ValuesQuerySet[_T, datetime.datetime]: ...
114+
) -> QuerySet[_T, datetime.datetime]: ...
119115
def none(self) -> QuerySet[_T]: ...
120116
def all(self) -> QuerySet[_T]: ...
121117
def filter(self, *args: Any, **kwargs: Any) -> QuerySet[_T]: ...

django-stubs/db/models/query.pyi

+49-47
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
import datetime
22
from collections.abc import AsyncIterator, Collection, Iterable, Iterator, MutableMapping, Sequence, Sized
3-
from typing import Any, Generic, NamedTuple, TypeVar, overload
3+
from typing import Any, Generic, NamedTuple, overload
44

55
from django.db.backends.utils import _ExecuteQuery
66
from django.db.models import Manager
77
from django.db.models.base import Model
88
from django.db.models.expressions import Combinable, OrderBy
99
from django.db.models.sql.query import Query, RawQuery
1010
from django.utils.functional import cached_property
11-
from typing_extensions import Self, TypeAlias
11+
from typing_extensions import Self, TypeAlias, TypeVar
1212

13-
_T = TypeVar("_T", bound=Model, covariant=True)
14-
_Row = TypeVar("_Row", covariant=True)
13+
_T = TypeVar("_T", covariant=True)
14+
_Model = TypeVar("_Model", bound=Model, covariant=True)
15+
_Row = TypeVar("_Row", covariant=True, default=_Model) # ONLY use together with _Model
1516
_QS = TypeVar("_QS", bound=_QuerySet)
1617
_TupleT = TypeVar("_TupleT", bound=tuple[Any, ...], covariant=True)
1718

1819
MAX_GET_RESULTS: int
1920
REPR_OUTPUT_SIZE: int
2021

21-
class BaseIterable(Generic[_Row]):
22+
class BaseIterable(Generic[_T]):
2223
queryset: QuerySet[Model]
2324
chunked_fetch: bool
2425
chunk_size: int
2526
def __init__(self, queryset: QuerySet[Model], chunked_fetch: bool = ..., chunk_size: int = ...) -> None: ...
26-
def __aiter__(self) -> AsyncIterator[_Row]: ...
27+
def __aiter__(self) -> AsyncIterator[_T]: ...
2728

28-
class ModelIterable(Generic[_T], BaseIterable[_T]):
29-
def __iter__(self) -> Iterator[_T]: ...
29+
class ModelIterable(Generic[_Model], BaseIterable[_Model]):
30+
def __iter__(self) -> Iterator[_Model]: ...
3031

3132
class RawModelIterable(BaseIterable[dict[str, Any]]):
3233
def __iter__(self) -> Iterator[dict[str, Any]]: ...
@@ -40,11 +41,11 @@ class ValuesListIterable(BaseIterable[_TupleT]):
4041
class NamedValuesListIterable(ValuesListIterable[NamedTuple]):
4142
def __iter__(self) -> Iterator[NamedTuple]: ...
4243

43-
class FlatValuesListIterable(BaseIterable[_Row]):
44-
def __iter__(self) -> Iterator[_Row]: ...
44+
class FlatValuesListIterable(BaseIterable[_T]):
45+
def __iter__(self) -> Iterator[_T]: ...
4546

46-
class _QuerySet(Generic[_T, _Row], Iterable[_Row], Sized):
47-
model: type[_T]
47+
class QuerySet(Generic[_Model, _Row], Iterable[_Row], Sized):
48+
model: type[_Model]
4849
query: Query
4950
_iterable_class: type[BaseIterable]
5051
_result_cache: list[_Row] | None
@@ -56,14 +57,14 @@ class _QuerySet(Generic[_T, _Row], Iterable[_Row], Sized):
5657
hints: dict[str, Model] | None = ...,
5758
) -> None: ...
5859
@classmethod
59-
def as_manager(cls) -> Manager[_T]: ...
60+
def as_manager(cls) -> Manager[_Model]: ...
6061
def __len__(self) -> int: ...
6162
def __bool__(self) -> bool: ...
62-
def __class_getitem__(cls: type[_QS], item: type[_T]) -> type[_QS]: ...
63+
def __class_getitem__(cls: type[_QS], item: type[_Model]) -> type[_QS]: ...
6364
def __getstate__(self) -> dict[str, Any]: ...
6465
# Technically, the other QuerySet must be of the same type _T, but _T is covariant
65-
def __and__(self, other: _QuerySet[_T, _Row]) -> Self: ...
66-
def __or__(self, other: _QuerySet[_T, _Row]) -> Self: ...
66+
def __and__(self, other: QuerySet[_Model, _Row]) -> Self: ...
67+
def __or__(self, other: QuerySet[_Model, _Row]) -> Self: ...
6768
# IMPORTANT: When updating any of the following methods' signatures, please ALSO modify
6869
# the corresponding method in BaseManager.
6970
def iterator(self, chunk_size: int | None = ...) -> Iterator[_Row]: ...
@@ -72,44 +73,46 @@ class _QuerySet(Generic[_T, _Row], Iterable[_Row], Sized):
7273
async def aaggregate(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
7374
def get(self, *args: Any, **kwargs: Any) -> _Row: ...
7475
async def aget(self, *args: Any, **kwargs: Any) -> _Row: ...
75-
def create(self, **kwargs: Any) -> _T: ...
76-
async def acreate(self, **kwargs: Any) -> _T: ...
76+
def create(self, **kwargs: Any) -> _Model: ...
77+
async def acreate(self, **kwargs: Any) -> _Model: ...
7778
def bulk_create(
7879
self,
79-
objs: Iterable[_T],
80+
objs: Iterable[_Model],
8081
batch_size: int | None = ...,
8182
ignore_conflicts: bool = ...,
8283
update_conflicts: bool = ...,
8384
update_fields: Collection[str] | None = ...,
8485
unique_fields: Collection[str] | None = ...,
85-
) -> list[_T]: ...
86+
) -> list[_Model]: ...
8687
async def abulk_create(
8788
self,
88-
objs: Iterable[_T],
89+
objs: Iterable[_Model],
8990
batch_size: int | None = ...,
9091
ignore_conflicts: bool = ...,
9192
update_conflicts: bool = ...,
9293
update_fields: Collection[str] | None = ...,
9394
unique_fields: Collection[str] | None = ...,
94-
) -> list[_T]: ...
95-
def bulk_update(self, objs: Iterable[_T], fields: Iterable[str], batch_size: int | None = ...) -> int: ...
96-
async def abulk_update(self, objs: Iterable[_T], fields: Iterable[str], batch_size: int | None = ...) -> int: ...
97-
def get_or_create(self, defaults: MutableMapping[str, Any] | None = ..., **kwargs: Any) -> tuple[_T, bool]: ...
95+
) -> list[_Model]: ...
96+
def bulk_update(self, objs: Iterable[_Model], fields: Iterable[str], batch_size: int | None = ...) -> int: ...
97+
async def abulk_update(
98+
self, objs: Iterable[_Model], fields: Iterable[str], batch_size: int | None = ...
99+
) -> int: ...
100+
def get_or_create(self, defaults: MutableMapping[str, Any] | None = ..., **kwargs: Any) -> tuple[_Model, bool]: ...
98101
async def aget_or_create(
99102
self, defaults: MutableMapping[str, Any] | None = ..., **kwargs: Any
100-
) -> tuple[_T, bool]: ...
103+
) -> tuple[_Model, bool]: ...
101104
def update_or_create(
102105
self,
103106
defaults: MutableMapping[str, Any] | None = ...,
104107
create_defaults: MutableMapping[str, Any] | None = ...,
105108
**kwargs: Any,
106-
) -> tuple[_T, bool]: ...
109+
) -> tuple[_Model, bool]: ...
107110
async def aupdate_or_create(
108111
self,
109112
defaults: MutableMapping[str, Any] | None = ...,
110113
create_defaults: MutableMapping[str, Any] | None = ...,
111114
**kwargs: Any,
112-
) -> tuple[_T, bool]: ...
115+
) -> tuple[_Model, bool]: ...
113116
def earliest(self, *fields: str | OrderBy) -> _Row: ...
114117
async def aearliest(self, *fields: str | OrderBy) -> _Row: ...
115118
def latest(self, *fields: str | OrderBy) -> _Row: ...
@@ -118,8 +121,8 @@ class _QuerySet(Generic[_T, _Row], Iterable[_Row], Sized):
118121
async def afirst(self) -> _Row | None: ...
119122
def last(self) -> _Row | None: ...
120123
async def alast(self) -> _Row | None: ...
121-
def in_bulk(self, id_list: Iterable[Any] | None = ..., *, field_name: str = ...) -> dict[Any, _T]: ...
122-
async def ain_bulk(self, id_list: Iterable[Any] | None = ..., *, field_name: str = ...) -> dict[Any, _T]: ...
124+
def in_bulk(self, id_list: Iterable[Any] | None = ..., *, field_name: str = ...) -> dict[Any, _Model]: ...
125+
async def ain_bulk(self, id_list: Iterable[Any] | None = ..., *, field_name: str = ...) -> dict[Any, _Model]: ...
123126
def delete(self) -> tuple[int, dict[str, int]]: ...
124127
async def adelete(self) -> tuple[int, dict[str, int]]: ...
125128
def update(self, **kwargs: Any) -> int: ...
@@ -138,13 +141,13 @@ class _QuerySet(Generic[_T, _Row], Iterable[_Row], Sized):
138141
using: str | None = ...,
139142
) -> RawQuerySet: ...
140143
# The type of values may be overridden to be more specific in the mypy plugin, depending on the fields param
141-
def values(self, *fields: str | Combinable, **expressions: Any) -> _QuerySet[_T, dict[str, Any]]: ...
144+
def values(self, *fields: str | Combinable, **expressions: Any) -> QuerySet[_Model, dict[str, Any]]: ...
142145
# The type of values_list may be overridden to be more specific in the mypy plugin, depending on the fields param
143-
def values_list(self, *fields: str | Combinable, flat: bool = ..., named: bool = ...) -> _QuerySet[_T, Any]: ...
144-
def dates(self, field_name: str, kind: str, order: str = ...) -> _QuerySet[_T, datetime.date]: ...
146+
def values_list(self, *fields: str | Combinable, flat: bool = ..., named: bool = ...) -> QuerySet[_Model, Any]: ...
147+
def dates(self, field_name: str, kind: str, order: str = ...) -> QuerySet[_Model, datetime.date]: ...
145148
def datetimes(
146149
self, field_name: str, kind: str, order: str = ..., tzinfo: datetime.tzinfo | None = ...
147-
) -> _QuerySet[_T, datetime.datetime]: ...
150+
) -> QuerySet[_Model, datetime.datetime]: ...
148151
def none(self) -> Self: ...
149152
def all(self) -> Self: ...
150153
def filter(self, *args: Any, **kwargs: Any) -> Self: ...
@@ -173,7 +176,7 @@ class _QuerySet(Generic[_T, _Row], Iterable[_Row], Sized):
173176
tables: Sequence[str] | None = ...,
174177
order_by: Sequence[str] | None = ...,
175178
select_params: Sequence[Any] | None = ...,
176-
) -> _QuerySet[Any, Any]: ...
179+
) -> QuerySet[Any, Any]: ...
177180
def reverse(self) -> Self: ...
178181
def defer(self, *fields: Any) -> Self: ...
179182
def only(self, *fields: Any) -> Self: ...
@@ -192,7 +195,7 @@ class _QuerySet(Generic[_T, _Row], Iterable[_Row], Sized):
192195
def __getitem__(self, s: slice) -> Self: ...
193196
def __reversed__(self) -> Iterator[_Row]: ...
194197

195-
class RawQuerySet(Iterable[_T], Sized):
198+
class RawQuerySet(Iterable[_Model], Sized):
196199
query: RawQuery
197200
def __init__(
198201
self,
@@ -205,28 +208,27 @@ class RawQuerySet(Iterable[_T], Sized):
205208
hints: dict[str, Model] | None = ...,
206209
) -> None: ...
207210
def __len__(self) -> int: ...
208-
def __iter__(self) -> Iterator[_T]: ...
211+
def __iter__(self) -> Iterator[_Model]: ...
209212
def __bool__(self) -> bool: ...
210213
@overload
211-
def __getitem__(self, k: int) -> _T: ...
214+
def __getitem__(self, k: int) -> _Model: ...
212215
@overload
213216
def __getitem__(self, k: str) -> Any: ...
214217
@overload
215-
def __getitem__(self, k: slice) -> RawQuerySet[_T]: ...
218+
def __getitem__(self, k: slice) -> RawQuerySet[_Model]: ...
216219
@cached_property
217220
def columns(self) -> list[str]: ...
218221
@property
219222
def db(self) -> str: ...
220-
def iterator(self) -> Iterator[_T]: ...
223+
def iterator(self) -> Iterator[_Model]: ...
221224
@cached_property
222225
def model_fields(self) -> dict[str, str]: ...
223-
def prefetch_related(self, *lookups: Any) -> RawQuerySet[_T]: ...
226+
def prefetch_related(self, *lookups: Any) -> RawQuerySet[_Model]: ...
224227
def resolve_model_init_order(self) -> tuple[list[str], list[int], list[tuple[str, int]]]: ...
225-
def using(self, alias: str | None) -> RawQuerySet[_T]: ...
226-
227-
_QuerySetAny: TypeAlias = _QuerySet # noqa: PYI047
228+
def using(self, alias: str | None) -> RawQuerySet[_Model]: ...
228229

229-
QuerySet: TypeAlias = _QuerySet[_T, _T]
230+
# Deprecated alias of QuerySet, for compatibility only.
231+
_QuerySet: TypeAlias = QuerySet
230232

231233
class Prefetch:
232234
prefetch_through: str
@@ -240,8 +242,8 @@ class Prefetch:
240242
def get_current_to_attr(self, level: int) -> tuple[str, str]: ...
241243
def get_current_queryset(self, level: int) -> QuerySet | None: ...
242244

243-
def prefetch_related_objects(model_instances: Iterable[_T], *related_lookups: str | Prefetch) -> None: ...
244-
async def aprefetch_related_objects(model_instances: Iterable[_T], *related_lookups: str | Prefetch) -> None: ...
245+
def prefetch_related_objects(model_instances: Iterable[_Model], *related_lookups: str | Prefetch) -> None: ...
246+
async def aprefetch_related_objects(model_instances: Iterable[_Model], *related_lookups: str | Prefetch) -> None: ...
245247
def get_prefetcher(instance: Model, through_attr: str, to_attr: str) -> tuple[Any, Any, bool, bool]: ...
246248

247249
class InstanceCheckMeta(type): ...

ext/django_stubs_ext/aliases.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
from django.utils.functional import _StrOrPromise as StrOrPromise
66
from django.utils.functional import _StrPromise as StrPromise
77

8+
# Deprecated type aliases. Use the QuerySet class directly instead.
89
QuerySetAny = _QuerySet
910
ValuesQuerySet = _QuerySet
1011
else:
1112
from django.db.models.query import QuerySet
1213
from django.utils.functional import Promise as StrPromise
1314

15+
StrOrPromise = typing.Union[str, StrPromise]
16+
# Deprecated type aliases. Use the QuerySet class directly instead.
1417
QuerySetAny = QuerySet
1518
ValuesQuerySet = QuerySet
16-
StrOrPromise = typing.Union[str, StrPromise]
1719

1820
__all__ = ["StrOrPromise", "StrPromise", "QuerySetAny", "ValuesQuerySet"]

mypy_django_plugin/lib/fullnames.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
DUMMY_SETTINGS_BASE_CLASS = "django.conf._DjangoConfLazyObject"
1515
AUTH_USER_MODEL_FULLNAME = "django.conf.settings.AUTH_USER_MODEL"
1616

17-
QUERYSET_CLASS_FULLNAME = "django.db.models.query._QuerySet"
17+
QUERYSET_CLASS_FULLNAME = "django.db.models.query.QuerySet"
1818
BASE_MANAGER_CLASS_FULLNAME = "django.db.models.manager.BaseManager"
1919
MANAGER_CLASS_FULLNAME = "django.db.models.manager.Manager"
2020
RELATED_MANAGER_CLASS = "django.db.models.fields.related_descriptors.RelatedManager"

mypy_django_plugin/transformers/managers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def example(self, a: T2) -> T_2: ...
216216
return False
217217

218218
if type_info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
219-
# If it is a subclass of _QuerySet, it is compatible.
219+
# If it is a subclass of QuerySet, it is compatible.
220220
return True
221221
# check that at least one base is a subclass of queryset with Generic type vars
222222
return any(_has_compatible_type_vars(sub_base.type) for sub_base in type_info.bases)

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def find_stub_files(name: str) -> List[str]:
2626
"django-stubs-ext>=5.0.0",
2727
"tomli; python_version < '3.11'",
2828
# Types:
29-
"typing-extensions",
29+
"typing-extensions>=4.11.0",
3030
"types-PyYAML",
3131
]
3232

tests/typecheck/contrib/admin/test_decorators.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
def method_action_invalid_fancy(self, request: HttpRequest, queryset: int) -> None: ...
109109
110110
def method(self) -> None:
111-
reveal_type(self.method_action_bare) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query._QuerySet[main.MyModel, main.MyModel])"
112-
reveal_type(self.method_action_fancy) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query._QuerySet[main.MyModel, main.MyModel])"
113-
reveal_type(self.method_action_http_response) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query._QuerySet[main.MyModel, main.MyModel]) -> django.http.response.HttpResponse"
114-
reveal_type(self.method_action_file_response) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query._QuerySet[main.MyModel, main.MyModel]) -> django.http.response.FileResponse"
111+
reveal_type(self.method_action_bare) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query.QuerySet[main.MyModel, main.MyModel])"
112+
reveal_type(self.method_action_fancy) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query.QuerySet[main.MyModel, main.MyModel])"
113+
reveal_type(self.method_action_http_response) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query.QuerySet[main.MyModel, main.MyModel]) -> django.http.response.HttpResponse"
114+
reveal_type(self.method_action_file_response) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query.QuerySet[main.MyModel, main.MyModel]) -> django.http.response.FileResponse"

tests/typecheck/contrib/admin/test_options.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@
147147
pass
148148
149149
class A(admin.ModelAdmin):
150-
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[Any, HttpRequest, _QuerySet[Any, Any]], Optional[HttpResponseBase]], str]" [list-item]
150+
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[Any, HttpRequest, QuerySet[Any, Any]], Optional[HttpResponseBase]], str]" [list-item]
151151
- case: errors_for_invalid_model_admin_generic
152152
main: |
153153
from django.contrib.admin import ModelAdmin

tests/typecheck/contrib/sitemaps/test_generic_sitemap.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
main:26: error: Argument 1 of "location" is incompatible with supertype "Sitemap"; supertype defines the argument type as "Offer" [override]
5050
main:26: note: This violates the Liskov substitution principle
5151
main:26: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
52-
main:40: error: Argument 1 to "GenericSitemap" has incompatible type "Dict[str, List[int]]"; expected "Mapping[str, Union[datetime, _QuerySet[Offer, Offer], str]]" [arg-type]
52+
main:40: error: Argument 1 to "GenericSitemap" has incompatible type "Dict[str, List[int]]"; expected "Mapping[str, Union[datetime, QuerySet[Offer, Offer], str]]" [arg-type]
5353
5454
installed_apps:
5555
- myapp

0 commit comments

Comments
 (0)