Skip to content
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

"Invalid base class" raised when Base Class is a class field #3587

Open
rowillia opened this issue Jun 21, 2017 · 8 comments
Open

"Invalid base class" raised when Base Class is a class field #3587

rowillia opened this issue Jun 21, 2017 · 8 comments
Labels
needs discussion topic-inheritance Inheritance and incompatible overrides

Comments

@rowillia
Copy link
Contributor

Repro:

class Foo:
    pass

Base = Foo

class Types:
    Base = Foo

reveal_type(Base)
reveal_type(Types.Base)

class Works(Base):
    pass

class Fails(Types.Base):
    pass

Results in:

test_cast.py:9: error: Revealed type is 'def () -> test_cast.Foo'
test_cast.py:10: error: Revealed type is 'def () -> test_cast.Foo'
test_cast.py:15: error: Invalid type "Base"
test_cast.py:15: error: Invalid base class

What's odd is that mypy recognizes Base and Types.Base as having identical types yet accepts Base as a base class but not Types.Base

@gvanrossum
Copy link
Member

gvanrossum commented Jun 21, 2017 via email

@rowillia
Copy link
Contributor Author

@gvanrossum Thanks for taking a look! I suspected this one would be near impossible to fix but wanted to report it anyway 😄

@ilevkivskyi
Copy link
Member

I would say that in some sense mypy is actually right here (while the error might be clearer). There is a distinction between type aliases and variables with types Type[...]. The former are known statically, and can be used in the type context (including in base classes). The latter are dynamic (can be reassigned etc.) and are not allowed in the type context. Normally, the two are distinguished by an explicit annotation: something with an explicit type is a variable, and something without an explicit annotation is an alias.

Except at class scope, everything that appears there is automatically a variable, not an alias (there is an explicit code for this in semanal.py). This is maybe partially for historic reasons. This can be in principle changed (so that the original example would work) for example like Guido proposed, but this is not a simple change, plus it will require to also change the logic in checkmember.py.

@gvanrossum
Copy link
Member

Hm, I didn't realize there was this exception. It makes some sense and I think I've seen code using it, having a base class that delegates certain things to another class and allowing a subclass to override the delegate class. I don't know if that case is more common than Roy's example (the latter looks a lot like abuse of a class as a namespace).

@wkschwartz
Copy link
Contributor

having a base class that delegates certain things to another class and allowing a subclass to override the delegate class

That's my use case that got me to run into this bug. It would be nice for this to work. Maybe one way to address #3587 (comment) is to detect when the class attribute that is being subclassed is marked as a typing.ClassVar?

Work around

Turn the class attribute pointing at another class into an inner class, like FooShadow below:

class Foo:
    pass

class Types:
    class FooShadow(Foo): pass

class WorksAgain(Types.FooShadow):
    pass

(Mypy 0.620 has no output for the above code.)

@r-darwish
Copy link

I tried this workaround when creating a stub for flask-sqlalchemy:

stub:

class SQLAlchemy:
    def __init__(self):
        ...

    class Model:
        pass

real code:

db = SQLAlchemy()

class User(db.Model):
    pass

However, I still get "Invalid base class" when subclassing db.Model

@gnmerritt
Copy link

This seems to work (mypy passes, code runs) for me after following the instructions here to separate the model classes from the flask-sqlalchemy db instance.

Model: Any = declarative_base(cls=ModelBase)

class MyModel(Model):
    __tablename__ = 'foo'
   ...

@zedrdave
Copy link

I found this issue while looking for my own way to solve flask-sqlalchemy stubs headache, and while @gnmerritt's method seems like the easiest way out, I'm still wondering if there does exist a way to solve the use-class-from-instance-attribute version of the problem:

db = SQLAlchemy()

class User(db.Model):
    pass

Or, put as a self-contained example:

class A():
    pass

class B:
    def __init__(self):
        self.sub = A

myB = B()
class C(myB.sub): # Name 'myB.sub' is not defined
    pass

(any variations, such as assigning B().sub to an Alias, yields the same errors as described above…)

@AlexWaygood AlexWaygood added the topic-inheritance Inheritance and incompatible overrides label Apr 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs discussion topic-inheritance Inheritance and incompatible overrides
Projects
None yet
Development

No branches or pull requests

9 participants