pythonpython-typingwagtailpyright

Advice on using Wagtail (e.g. RichTextField) with Pylance type checking


Nearly all my Wagtail models files are full of errors according to Pylance and I'm not sure how to silence them without either adding # type: ignore to hundreds of lines or turning off Pylance rules that help catch genuine bugs. The errors often come from RichTextField properties on my models. Here is a simple example:

from wagtail.models import Page
from wagtail.fields import RichTextField

class SpecialPage(Page):
    introduction = RichTextField(
        blank=True,
        features=settings.RICHTEXT_ADVANCED,
        help_text="Text that appears at the top of the page.",
    )

where RICHTEXT_ADVANCED is a list[str] in my settings file. This code works fine. The arguments passed to RichTextField all exist in its __init__ method or the __init__ method of one a parent class. Yet Pylance in VS Code underlines all three lines of introduction in red and says:

No overloads for "__new__" match the provided arguments Argument types: (Literal[True], Any, Literal['Text that appears at the top of the page.']) Pylance(reportCallIssue)

Is this a bug in Pylance? Is there a way around it other than the two (undesirable) approaches I mentioned above? Or could Wagtail provide more explicit type hints or @overload indicators to prevent the errors?

The class inheritance goes RegisterLookupMixin > Field (has blank and help_text) > TextField (has features) > RichTextField. None of these classes have a __new__ method. The values I'm providing all match the types defined in the parent classes. I'm on a 5.x Wagtail, has this perhaps been fixed in more recent releases?


Solution

  • You're hitting a deficiency in pylance: the heuristic doesn't apply in your case and causes troubles.

    I don't have anything powered by pylance available to investigate this, but you may try with a smaller snippet to check if pylance thinks that def __init__(*args, **kwargs) on a subclass means "use same arguments as parent". This is often true, but also often wrong.

    class A:
        def __init__(self, foo):
            self.foo = foo
    
    
    class B(A):
        def __init__(self, *args, **kwargs):
            self.bar = kwargs.pop("bar")
            super().__init__(*args, **kwargs)
    
    B(foo=0, bar=1)
    

    Pyright accepts this code, so the problem is most likely in its wrapper - pylance.

    Neither Wagtail nor Django are type hinted, so this example is representative of what you observe. RichTextField defines a catch-all args, kwargs constructor, so pyright looks further up the inheritance chain. All the way to django.db.models.TextField with def __init__(self, *args, db_collation=None, **kwargs) and finally up to plain Field that defines all arguments explicitly here.

    Now, this should be possible to circumvent somehow, right? Right?..

    from typing import TYPE_CHECKING
    
    from django.db.models.fields import NOT_PROVIDED
    from wagtail.fields import RichTextField
    
    class MyRichTextField(RichTextField):
        if TYPE_CHECKING:  # False at runtime
            def __init__(
                self,
                verbose_name=None,
                name=None,
                primary_key=False,
                max_length=None,
                unique=False,
                blank=False,
                null=False,
                db_index=False,
                rel=None,
                default=NOT_PROVIDED,
                editable=True,
                serialize=True,
                unique_for_date=None,
                unique_for_month=None,
                unique_for_year=None,
                choices=None,
                help_text="",
                db_column=None,
                db_tablespace=None,
                auto_created=False,
                validators=(),
                error_messages=None,
                db_comment=None,
                db_default=NOT_PROVIDED,
                *,
                # from TextField
                db_collation=None,
                # from RichTextField
                editor="default",
                features=None,
            ): ...
    

    You may add annotations to the fields you're going to use, if you'd like to. If you want, django-stubs already define type hints for these arguments, right here - you can use that for reference.