Skip to content

Replace TypeAlias _ClassLevelWidgetT with descriptor _WidgetTypeOrInstance #2615

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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

michalpokusa
Copy link
Contributor

I have made things!

Related issues

None I could find.

This change allows for recognizing when .widget is used on Field instance and when on the type itself, allowing for different type annotations in each case.

image
image
image

Why even bother?
Example: Currently this correct code does not recognize is_required and attrs on field.widget, because it might be a type and not an instance.
image

After the change all types are correctly inferred, and syntax highlighting works as expected:
image

I hope you find it worth adding.

Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a good idea! Thanks!

Please, fix the CI :)

@michalpokusa michalpokusa force-pushed the field-widget-rework branch 2 times, most recently from f0c7575 to 097bd62 Compare April 21, 2025 15:23
@michalpokusa michalpokusa changed the title Replace TypeAlias _ClassLevelWidgetT with descriptor WidgetTypeOrInstance Replace TypeAlias _ClassLevelWidgetT with descriptor _WidgetTypeOrInstance Apr 21, 2025
@michalpokusa
Copy link
Contributor Author

@sobolevn I think I might need some help there. I checked why the CI is failing and it looks like errors are not even slightly related to what I have changed.

I checkouted the current origin/HEAD, ran the test locally and also I got a lot of errors. Is there something obvious I am missing? I believe there might be a problem with the CI itself as I see that on other PR #2616 also fails in the same way.

@UnknownPlatypus
Copy link
Contributor

Setuptools version 79 got installed, I suppose it probably breaks the multiple SETUPTOOLS_ENABLE_FEATURES in ci

@sobolevn
Copy link
Member

you can pin older setuptools for now in a separate PR :)

Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please also add several type assertions for this? https://github.com/typeddjango/django-stubs/tree/master/tests/assert_type

@michalpokusa
Copy link
Contributor Author

michalpokusa commented Apr 22, 2025

@sobolevn I started adding type assertions, and I would like to get you point of view on one thing. I could leave this as typing everything as the most general Widget, or I could make it generic, and type widget individually for each field type.

Example:
image
image
image
image

It provides more precise typing, but I am not sure whether it is not too restrictive.
On the other hand MultipleChoiceField could have a widget of type SelectMultiple or CheckboxSelectMultiple.
And how should it work with custom field types and custom widgets, wouldn't it break them?

What do you think about that?

@michalpokusa michalpokusa marked this pull request as draft April 22, 2025 22:05
@sobolevn
Copy link
Member

Let's first add the simplest typing possible. We can later update it to cover more cases.

@michalpokusa
Copy link
Contributor Author

I have added assert type tests.

Seems like this is a bit more complex than I initailly assumed, right now there is a problem with stubtest.
All the errors center around widgets being MediaDefiningClass instances. The interesting thing is that e.g. Field.hidden_widget is typed as type[Widget], while BaseFormSet.deletion_widget as MediaDefiningClass inside ".pyi" files.

Any further guidance would be highly appreciated.

@sobolevn
Copy link
Member

Based on the amount of failures I would say that this seems overly complex to type properly :(

However, I am open to ideas.

@michalpokusa
Copy link
Contributor Author

Would it be possible to supress these errors? It seems like they are a side affect of some problem with django/mypy itself.
I suppose it is related to the Widget(metaclass=MediaDefiningClass), but the Widget instance is not a MediaDefiningClass subclass.

image
image

I tried to fix the errors in .pyi files, but the errors seem to be simply inaccurate. It they could be supressed, users could benefit from the better typing while not technically lying about the typing to mypy, as it seems like it gets it wrong in this case.

Any opinion on that would be appreciated.

@UnknownPlatypus
Copy link
Contributor

Would it be possible to supress these errors? It seems like they are a side affect of some problem with django/mypy itself.
I suppose it is related to the Widget(metaclass=MediaDefiningClass), but the Widget instance is not a MediaDefiningClass subclass.

I think it might be actual error for the types when working with a field class, not an instance of a field. I do agree the error message is a bit weird tho but the def (attrs=None) signature is Widget.__init__, which makes me think the type is incorrect for subclasses.

Could you add a test with a custom Field using a custom widget ? Something like that:

class MyField(Field):
    widget = Input

I think it will fail for the same reason as in #2650, I couldn't find a way to make it work with this approach for a similar need, maybe you'll find a way. Basically the approach with a descriptor makes it mostly work when working with instances of Field but breaks for the type.

@michalpokusa
Copy link
Contributor Author

michalpokusa commented May 12, 2025

I was able to pass the stubtests locally by typing e.g.

class IntegerField(Field):
    widget: _WidgetTypeOrInstance[NumberInput] | MediaDefiningClass

I changed _WidgetTypeOrInstance here to be a generic, but this works on non-generic version also.

Although the typing is not really correct in this case, because the widget is never a MediaDefiningClass instance, it still allows for better syntax highlighting than currently:
image

This is how the exact same code would look like with current typing:
image

@michalpokusa michalpokusa force-pushed the field-widget-rework branch from c84dd6b to fc18a3f Compare May 12, 2025 02:03
@michalpokusa
Copy link
Contributor Author

michalpokusa commented May 12, 2025

After a few hours of researching and testing multiple approaches, which mainly included trying to overcome problems with mypy, I finally came up with current version.

So, right now:

  • the _WidgetTypeOrInstance is generic, and should type hint not only that field.widget is Widget, but also what type of widget subclass it is. I showed the screenshot in earlier comment Replace TypeAlias _ClassLevelWidgetT with descriptor _WidgetTypeOrInstance #2615 (comment)

  • the generic descriptor accepts separate widget types for class and instance, why is that, because of subclasses of django.contrib.postgres.forms.ranges.BaseRangeField
    image
    this is related to the Django implementation:

def __init__(self, **kwargs):
    if "widget" not in kwargs:
        kwargs["widget"] = RangeWidget(self.base_field.widget)
  • I added all widgets that caused stubtest errors to allowlist.txt with annotation why they are there, and I added # type: ignore[assignment] in places where I override the typing for widget in field subclasses

Overall, all the tests pass, and I feel much more confident about this version rather than trying to make mypy happy, even when it is clearly wrong with its detection.

@sobolevn I would really appreciate your opinion on this, I believe it fits with the rest of the django-stubs codebase and that the use of allowlist.txt is justified in this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants