-
-
Notifications
You must be signed in to change notification settings - Fork 454
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
Improve types of django.contrib.admin.decorators
#1267
Conversation
function: Callable[[_ModelAdmin, _Request, _QuerySet], None], | ||
permissions: Sequence[str] | None = ..., | ||
description: _StrPromise | None = ..., | ||
) -> Callable[[_ModelAdmin, _Request, _QuerySet], None]: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be easier and less repetition to define
_ActionCallable = TypeVar("_ActionCallable", bound=Callable[[ModelAdmin, HttpRequest, QuerySet], None])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not gonna work. It needs a bound, if you declare them without a TypeVar
it's gonna change the signature of the function that is decorated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, right, the bound callable won't match either. I started out with that but then you'll need nested TypeVars to get a correct bound. Else we're not supporting subclasses. See here
https://mypy-play.net/?mypy=0.982&python=3.10&gist=de1239d0a4ced1707ec6afb9f4610160
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks cluttered, I agree, but this was the only way I could get it to work correctly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you manage to figure out how to declare a TypeVar(..., bound=Callable[...])
that works.
Pass me a playground link showing how, I'm very interested in knowing how to with mypy (I want to use it in other projects🙂)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I understand now. Ordinarily with bound=Callable[[Model], ...]
, the type system applies contravariant type compatibility to arguments of the callable: to be type-safe, the callable must accept every possible Model object that could be passed in. (explanation)
What you're trying to do here is covariant behavior: the callable should accept some subclass of Model
as argument. A TypeVar
is bound checking is covariant. Which is the correct subclass in the particular instance won't be type-checked.
It's a bit of a hack and not entirely type-safe, but I think practicality beats purity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[...] Which is the correct subclass in the particular instance won't be type-checked.
It's a bit of a hack and not entirely type-safe, but I think practicality beats purity.
Could you elaborate on what it is that won't be type checked and why it's not type safe? (I presume with the current display annotations func: Callable[[_ModelT], _T]
)
I'm not sure I'm following on how that conclusion is reached.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example given two models Foo
and Bar
. With your proposed solution, you may have a FooAdmin
class with a @display
method def my_display_func(obj: Bar)
. The type checker has no idea that Bar
is incorrect and the method will actually accept Foo
instances. That's what I meant by "not type-safe"
The "type-safe" approach is contravariant compatibility: you can define my_display_func(obj: Model)
or my_display_func(obj: object)
, and it's assured that any passed argument is always an instance of Model
or object
. It's type-safe, but useless: almost every display callback needs to access fields of the particular Model subclass, you'd need to use cast()
or isinstance()
guards to do most useful things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm yeah that's true. Didn't think of that.
I suppose in order to get around that, it would be necessary for admin.display
to have the context of FooAdmin
(e.g. FooAdmin.model
), although it's a naive, dynamic, decorator that doesn't even consider the ModelAdmin.
What I'm saying is e.g. @admin.display[MyModelAdmin]
(if ModelAdmin accepted arguments runtime) and pick up its model. But then it also has to be possible to pick up the Model argument for ModelAdmin
_T = TypeVar("_T") | ||
_Model = TypeVar("_Model", bound=Model) | ||
_ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin) | ||
_Request = TypeVar("_Request", bound=HttpRequest) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only remaining dilemma I have is whether the request argument should be a TypeVar, or change it to just HttpRequest
without TypeVar.
It seems most django-stubs code already assumes that requests are always HttpRequest
and that tends to be sufficient.
Technically most of the time they're WSGIRequest
, but that seems to be an implementation detail. There is no explicit documentation about WSGIRequest
in Django docs (barring testing topics and release notes). And ASGIRequest
is now now rearing its head.
Unless someone else chimes in, I'll trust your judgment here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the type-unsafety you've already stated still stands. Having this, it should be possible for someone to do
@action(...)
def my_action(..., request: MyCustomHttpRequest, ...) -> None:
request.attribute_set_by_middleware
e.g. Capturing attributes attached by middleware(s) or so.
Or doing stuff mentioned in docs: https://github.com/typeddjango/django-stubs#how-can-i-create-a-httprequest-thats-guaranteed-to-have-an-authenticated-user
Sorry to delay this for so long. Since this change is fairly complex, I'd like to see a test case for these as well. Look in With that, I'm happy to approve this. |
Thanks! I played around with this and created some test cases. I'm going to merge this and open another PR with the tests and some additional tweaks. |
Sorry for being so slow, I've had a couple of test cases locally since I built this. Just never came around to pushing it |
Sorry if I was too impatient. Maybe some advance heads-up would have been warranted. If you don't mind, have a look at #1292 and see if I missed anything. |
Not to worry, it's me being slow. I'll try to have a look tomorrow |
Some follow-up additions on top of #1267: * Added tests * The `@display` decorator now supports usage for `ModelAdmin` methods as well. * Changed `_StrPromise` to `_StrOrPromise` where plain `str` should be supported as well. * `ModelAdmin.display` didn't previously allow functions returning booleans. * `ModelAdmin.actions` didn't previously allow having accurate type hints of the passed function (only allowed base class `ModelAdmin`)
I have made things!
django.contrib.admin.decorators
previously changed the signature of the classes/functions that were decorated. Not any more.description=
arguments now supports bothPromise
andstr
->_StrPromise
(forgettext_lazy
support)Related issues
None