django-stubs icon indicating copy to clipboard operation
django-stubs copied to clipboard

Transform user_passes_test signature

Open ljodal opened this issue 2 years ago • 19 comments

Add a transformer that updates the signature of user_passes_test to reflect settings.AUTH_USER_MODEL.

Related issues

Fixes #1058

ljodal avatar Nov 03 '22 22:11 ljodal

This seems like significant complexity to dynamically update the signature of just one function.

There are lots more places in Django that depend on AUTH_USER_MODEL. Are you planning to extend this and add update the other places as well, and can this be implemented generically?

request.user seems like an important one for example.

intgr avatar Nov 04 '22 09:11 intgr

This seems like significant complexity to dynamically update the signature of just one function.

There are lots more places in Django that depend on AUTH_USER_MODEL. Are you planning to extend this and add update the other places as well, and can this be implemented generically?

request.user seems like an important one for example.

This is already done for request.user: https://github.com/typeddjango/django-stubs/blob/e88f942f861809d2b824ecc12b908c8c90bbf6d2/mypy_django_plugin/transformers/request.py#L10

Same with get_user_model(): https://github.com/typeddjango/django-stubs/blob/e88f942f861809d2b824ecc12b908c8c90bbf6d2/mypy_django_plugin/transformers/settings.py#L11

I'm not sure if there's an easy way to do this generically (I'm also unsure how many places this is needed). I've just fixed the only place where this was causing new type issues in our codebase

ljodal avatar Nov 04 '22 10:11 ljodal

I think instead we can make AUTH_USER_MODEL an actual type and use it everywhere 🤔

sobolevn avatar Nov 04 '22 10:11 sobolevn

I'm not exactly sure if I'm following how you'd envision that working? Altering the type of settings.AUTH_USER_MODEL to be a type alias? Wouldn't that cause issues when directly accessing that setting and expecting a string?

I'm happy to do some cleanup here and add a helper to extract the 'current' user type (to reduce duplication), but I don't see how this could be done in another way.

ljodal avatar Nov 07 '22 11:11 ljodal

I think we can at least experiment to do something like this:

  1. Import settings.AUTH_USER_MODEL model from runtime
  2. Pass it through our regular plugin machinery
  3. Redefine settings.AUTH_USER_MODEL to be an alias for that type

So, ideally we can change all (most) User references in stubs with AUTH_USER_MODEL type. Like this:

def user_passes_test(func: Callable[[AUTH_USER_MODEL | AnonymousUser], bool]) -> ...: ...`

However, I don't say that:

  1. It is a good idea, I am not completely sure how it can be implemented
  2. That it will work at all

sobolevn avatar Nov 07 '22 12:11 sobolevn

One immediate problem I see with that is that settings.USER_AUTH_MODEL should stay defined as a string, as someone might rely on that at runtime as well. I guess get_user_model() is the API to use to get the actual model. I don't think that would be accepted as a type though. Maybe we could add something like this somewhere, but I'm not sure where? And I'm not sure if it would even work?

User = get_user_model()

ljodal avatar Nov 07 '22 12:11 ljodal

It can work because of get_dynamic_class_hook(), but I am not sure this will be accepted as a type.

Ok, we can try to populate _USER_AUTH_MODEL magic thing 🤔

sobolevn avatar Nov 07 '22 12:11 sobolevn

Hmm, wouldn't that break usage of the stubs without the plugin? For this to work properly I'd think it has to be defined somewhere in the stubs at least?

ljodal avatar Nov 07 '22 13:11 ljodal

It can work because of get_dynamic_class_hook(), but I am not sure this will be accepted as a type.

Ok, we can try to populate _USER_AUTH_MODEL magic thing 🤔

I got a spin on this idea:

settings.AUTH_USER_MODEL is Django utilising its lazy referencing ("<app_label>.<object_name>"), and we also know that it looks for a registered model under the hood. Which means that (probably currently) we've typed or can assume type[Model] wherever settings.AUTH_USER_MODEL has been resolved.

So, what if the stubs add some variable: _AUTH_USER_MODEL = NewType("_AUTH_USER_MODEL", Type[Model])? That might perhaps trigger get_dynamic_class_hook and give us a statically declared type which is "bound" to be correct but the plugin is able to refine it into a even more specific type?

Then the stubs replaces all places where we expect settings.AUTH_USER_MODEL to have been resolved e.g.

- -> Type[Model]:
+ -> _AUTH_USER_MODEL:

Which (hopefully) would align all types.

flaeppe avatar Sep 19 '23 10:09 flaeppe

I like this idea (I think that adding a hook is a must, however - otherwise, we will have too many errors in users' code).

sobolevn avatar Sep 19 '23 10:09 sobolevn

Hmm, interesting. Sounds like it's worth a try! Maybe best to try in a separate PR though?

ljodal avatar Sep 19 '23 13:09 ljodal

Yeah a separate PR would be best

flaeppe avatar Sep 19 '23 17:09 flaeppe

Are we sure we can't just use get_auth_user() together with get_dynamic_class_hook() though? Something like this?

# stubs.pyi

User = get_auth_user()

class HttpRequest:
    user: User | AnonymousUser

ljodal avatar Sep 20 '23 08:09 ljodal

I think we should, because it is a perfect use-case for it.

sobolevn avatar Sep 20 '23 09:09 sobolevn

I don't think that works. Isn't it only TypeAlias that can be annotated with?

And that function call can't return a type alias, I've tried make the plugin spoof that kind of aliasing as well, in #1699

flaeppe avatar Sep 20 '23 11:09 flaeppe

Mypy makes User a variable(Var) and that can't be annotated with

flaeppe avatar Sep 20 '23 11:09 flaeppe

But can't we change that using get_dynamic_class_hook?

ljodal avatar Sep 20 '23 12:09 ljodal

But can't we change that using get_dynamic_class_hook?

Probably, I've tried that in #1699 but it isn't trivial

flaeppe avatar Sep 20 '23 12:09 flaeppe

I think I managed to make it work: #1730

Main trick was replacing the symbol with a placeholder when deferring, as otherwise the variable error popped up (that seems like a bug in mypy)

ljodal avatar Sep 21 '23 22:09 ljodal