Pylance-release: How can I disable the bundled SQLAlchemy stubs?

Created on 15 Jan 2021  路  11Comments  路  Source: microsoft/pylance-release

My code is incompatible with the SQLAlchemy stubs bundled in the latest version of Pylance. Is there a way to disable these via the config file or adding something to the typings directory?

Are these stubs identical to the latest release of https://github.com/dropbox/sqlalchemy-stubs or do they include functionality similar to the mypy plugin included in that repo?

enhancement

Most helpful comment

@jakebailey I can confirm that a copy of the stubs in typings takes precedence over the bundled stubs. I didn't attempt to disable them by putting an empty stub in typings.

After a lot of trial and error, I managed to get the sqlalchemy stubs working reasonably well with Pyright and Pylance. Since there's practically no information out there on how to do this, I'll share what I've found.

Flask-SQLAlchemy

If you're using Flask along with Flask-SQLAlchemy, you can't use the query attribute on the models.

use this:

db.session(User).get(id)

not this:

User.query.get(id)

Models

When defining models, columns must be explicitly cast to their associated Python types. When defining relationships, both sides of the relationship should be defined separately using back_populates as opposed to defining both sides on one model using backref.

class User(Base):
    id = cast(
        str,
        Column(
            "id", UUID(), primary_key=True, server_default=text("uuid_generate_v4()")
        ),
    )
    organization_id = cast(
        str, Column(UUID(), ForeignKey("organization.id"), index=True, nullable=False)
    )
    first_name = cast(Optional[str], Column(String))
    last_name = cast(Optional[str], Column(String))
    email = cast(str, Column(String, nullable=False))
    organization: 'RelationshipProperty["Organization"]' = relationship(
        "Organization", back_populates="users"
    )


class Organization(Base):
    id = cast(
        str,
        Column(
            "id", UUID(), primary_key=True, server_default=text("uuid_generate_v4()")
        ),
    )
    name = cast(str, Column(String, nullable=False))
    users: 'RelationshipProperty[List["User"]]' = relationship(
        "User", back_populates="organization"
    )

Queries

When querying using filter() or order_by() along with a method on a column, add # type: ignore to the end of the lines with the column method calls.

db.session.query(User)
.filter(User.id.in_(user_ids))  # type: ignore
.order_by(User.created_at.desc())  # type: ignore

List assignment

When assigning a list of models to another model, the list will need to be explicitly cast to RelationshipProperty[List[Model]]

organization.users = cast("RelationshipProperty[List[User]]", users)

All 11 comments

The stubs are identical to that repo, but without the mypy plugin (as we aren't mypy and can't use it).

We don't have a way to disable them, no, as we assumed they'd be better than trying to read the actual source code. As a workaround, you can just delete the folder from the extension folder itself and reload the editor.

Can you describe the incompatibility? Are you referring to type checking, or something else? How was your code passing beforehand when SQLAlchemy isn't typed?

I may just not be doing this right, but I can explain the behavior I see. Prior to the latest release of pylance, I had manually set all of the types for all of the column fields. For example, I might have:

class blah(base):
    id:int = Column(Integer)

I know this is technically wrong, but it make it so consumers of the code could see the right types when accessing the class. As of the latest pylance release, this generates the big red squiggles with an error that says Column[int] is not compatible with int.

Unfortunately I also have some classes where I never added the type hinting because they were more-or-less self contained:

class runRecord(base):
    username=Column(Text)
    runTime=Column(DateTime)
    def __init__(self):
        self.runTime = datetime.now()

When another part of the code tries to write a string to the username field, I get:

Cannot assign member "username" for type "runRecord"
  Expression of type "str" cannot be assigned to member "username" of class "runRecord"
    "str" is incompatible with "Column[Text]"

Small update: if you assign a value in the constructor, the type hint system appears to use that rather than the Column type. In other words if in runRecord.__init__ I had self.username = '' the type hinter doesn't generate squiggles. This makes perfect sense, but it seemed worth mentioning.

Is there a 'right' way to annotate these?

@archfear I'd still like to get some info about what's going on in your codebase. I'm not entirely certain it's the stubs, but potentially another change (maybe the same as below). Deleting the stubs would be a useful datapoint.

@smithed180 What you're seeing is likely an impact of #822; in the "off" type checking mode, there was a special case where we'd "allow" the bad type assignment in an effort to be more friendly. But, I'm a bit confused since in the "off" mode you shouldn't see that error message at all. Do you have type checking enabled?

If you wanted to annotate that and force the type to be int, I'd think you'd need a # type: ignore comment on that line to suppress the message, since it is giving a valid message by sating "you can't assign Column[int] to int as they aren't compatible. SQLAlchemy is unfortunate from the POV that you do a bunch of assignments to build up a class, but the code you write don't actually reflect the types that are there at runtime...

Thanks Jake -- it sounds like my original iffy way of handling things is still best. Pylance is still a ton better at inferring things...its nice when you argue with the tool about types and then find out that it was right and you had a bug, so dealing with sqlalchemy's weirdness is a small price to pay.

As for the 'off' mode, is it correct to assume you mean this setting? If so I've always had it set this way:
"python.analysis.typeCheckingMode": "basic"
I wasn't 100% what the options meant when I installed (off is described as having no type checking, while basic is "all rules in off + basic"...but I thought off had no rules....) So anyway I just picked basic because it sounded like "medium" and "medium" seemed like a fair starting point :)

Edit: is there a way to turn off type checking in either of these two ways:

  • An entire block. I have this class with N columns, I want to disable type checking for the block
  • Just a portion of a line. For example, if I have blah:int=column(Integer, thing1=True, thing2=1.23) I want the type checker to still check that thing1 and thing2 are the right types, just want it to ignore the overall assignment and let me claim that blah is an int.

sorry for hijacking a little bit

An entire block. I have this class with N columns, I want to disable type checking for the block

There isn't a way to do this. #282

Just a portion of a line. For example, if I have blah:int=column(Integer, thing1=True, thing2=1.23) I want the type checker to still check that thing1 and thing2 are the right types, just want it to ignore the overall assignment and let me claim that blah is an int.

You might be able to split it into many lines and then stick # type: ignore on just one of them, but that may not work either. For this case, the assignment is the problem, so as gross as it sounds, you could also do blah = cast(int, column(...)), but I know that feels wrong too.

@jakebailey My situation is similar to @smithed180. I was having issues getting Dropbox's SQLAlchemy stubs to work with Flask-SQLAlchemy and my app's architecture. I annotated the columns in the same way as @smithed180 to at least get type checking on the attributes of the model instance. This works fine in strict mode as long as the SQLAlchemy stubs aren't installed. I needed to add # type: ignore when calling a function on a model attribute such as in_ from within a filter() call and annotate the results of a query since they'll be untyped.

I'll be removing Flask-SQLAlchemy from my app in the future and plan to revisit getting the stubs working then.

I see, so previously SQLAlchemy was untyped, and now that it's typed you're getting mismatches? I'm curious what the inferencing was doing without the stubs, then, as if you were in strict mode I would have expected some errors for using types that were inferred. We had assumed that adding these stubs would only improve things.

Just to be clear, if you delete the sqlalchemy folder from the pylance extension, do things work as they previously did, or are there still errors?

You might be able to split it into many lines and then stick # type: ignore on just one of them, but that may not work either. For this case, the assignment is the problem, so as gross as it sounds, you could also do blah = cast(int, column(...)), but I know that feels wrong too.

Ah yeah should have just tried the multiline thing -- can confirm it does not work. The "1" here should (and does, without the ignore comment) cause a type error:

    id:int = Column( #type:ignore
        1, Integer)

This one still generates the error on the first line

    id = Column('id', Integer, primary_key=True, autoincrement=True, nullable=False)
    id: int #type:ignore

Most of the sqlalchemy things are keywords anyway so I'll just do the type ignore. Thanks for the assistance.

@jakebailey Removing the types from dist/bundled/stubs or downgrading the extension both fix the issue. I should be more clear about my config: typeCheckingMode is set to strict, but useLibraryCodeForTypes is false and I've disabled reportMissingTypeStubs and all the reportUntyped* and reportUnknown* rules.

Without the stubs, everything referenced from SQLAlchemy is inferred as Unknown other than the model attributes I explicitly annotated.

One possible workaround here would be to allow the stubs in a project's typings directory take precedence over the bundled stubs. This would allow for using a specific version of the SQLAlchemy stubs in a project or disabling them using the technique described here: https://github.com/microsoft/pyright/issues/945#issuecomment-686292537

The typings directory should definitely take precedence over the bundled stubs; the intended order is typings, then installed (search paths), then bundled as a last resort. Are you seeing a different behavior?

@jakebailey I can confirm that a copy of the stubs in typings takes precedence over the bundled stubs. I didn't attempt to disable them by putting an empty stub in typings.

After a lot of trial and error, I managed to get the sqlalchemy stubs working reasonably well with Pyright and Pylance. Since there's practically no information out there on how to do this, I'll share what I've found.

Flask-SQLAlchemy

If you're using Flask along with Flask-SQLAlchemy, you can't use the query attribute on the models.

use this:

db.session(User).get(id)

not this:

User.query.get(id)

Models

When defining models, columns must be explicitly cast to their associated Python types. When defining relationships, both sides of the relationship should be defined separately using back_populates as opposed to defining both sides on one model using backref.

class User(Base):
    id = cast(
        str,
        Column(
            "id", UUID(), primary_key=True, server_default=text("uuid_generate_v4()")
        ),
    )
    organization_id = cast(
        str, Column(UUID(), ForeignKey("organization.id"), index=True, nullable=False)
    )
    first_name = cast(Optional[str], Column(String))
    last_name = cast(Optional[str], Column(String))
    email = cast(str, Column(String, nullable=False))
    organization: 'RelationshipProperty["Organization"]' = relationship(
        "Organization", back_populates="users"
    )


class Organization(Base):
    id = cast(
        str,
        Column(
            "id", UUID(), primary_key=True, server_default=text("uuid_generate_v4()")
        ),
    )
    name = cast(str, Column(String, nullable=False))
    users: 'RelationshipProperty[List["User"]]' = relationship(
        "User", back_populates="organization"
    )

Queries

When querying using filter() or order_by() along with a method on a column, add # type: ignore to the end of the lines with the column method calls.

db.session.query(User)
.filter(User.id.in_(user_ids))  # type: ignore
.order_by(User.created_at.desc())  # type: ignore

List assignment

When assigning a list of models to another model, the list will need to be explicitly cast to RelationshipProperty[List[Model]]

organization.users = cast("RelationshipProperty[List[User]]", users)
Was this page helpful?
0 / 5 - 0 ratings