How to use my own Meta class together with SQLAlchemy-Model as a parent class

Question:

I recently started to use Flask as my back-end framework. However, recently I encountered a problem and I could not figure out how to solve it. As a last resort I wanted to try my change here. If you could help me with it, I would be grateful.

So, I have a class that inherits from SQLAlchemy’s db.Model:

from flask_sqlalchemy import SQLAlchemy


db = SQLAlchemy()


class User(db.Model):
  ...

And I would like to create my own Meta class to automatically insert some Mixins to this class:

class MyMetaClass(type):
  def __new__(meta, name, bases, class_dict):
    bases += (Mixin1, Mixin2, Mixin3)
    return type.__new__(meta, name, bases, class_dict)

However, when I try to use this Meta class with the User class:

class User(db.Model, metaclass=MyMetaClass):
   ...

I have the following error:

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Does anybody know how to solve this problem? Any help would be appreciated.

(Note: I am not a master of neither Flask nor Meta classes. So if I understood something wrong, please excuse my lack of understanding)

Asked By: Kıvanç Yüksel

||

Answers:

Finally I managed to solve my problem. In case anyone else encounters the same issue, I am posting a solution here.

This is a snipped that is taken from SQLAlchemy’s website:


The model metaclass is responsible for setting up the SQLAlchemy internals when defining model subclasses. Flask-SQLAlchemy adds some extra behaviors through mixins; its default metaclass, DefaultMeta, inherits them all.

  • BindMetaMixin: bind_key is extracted from the class and applied to the table. See Multiple Databases with Binds.
  • NameMetaMixin: If the model does not specify a tablename but does specify a primary key, a name is automatically generated.

You can add your own behaviors by defining your own metaclass and creating the declarative base yourself. Be sure to still inherit from the mixins you want (or just inherit from the default metaclass).

Passing a declarative base class instead of a simple model base class, as shown above, to base_class will cause Flask-SQLAlchemy to use this base instead of constructing one with the default metaclass.

from flask_sqlalchemy import SQLAlchemy
from flask_sqlalchemy.model import DefaultMeta, Model

class CustomMeta(DefaultMeta):
    def __init__(cls, name, bases, d):
        # custom class setup could go here

        # be sure to call super
        super(CustomMeta, cls).__init__(name, bases, d)

    # custom class-only methods could go here

db = SQLAlchemy(model_class=declarative_base(
    cls=Model, metaclass=CustomMeta, name='Model'))

You can also pass whatever other arguments you want to declarative_base() to customize the base class as needed.


In addition to that, my initial purpose was actually to add some extra functionality to my models which are inherits from db.Model. To do that actually you do not need to use Meta classes. So, for that case, we can extend the db.Model class, as described in the same web page. This is an example of giving every model an integer primary key, or a foreign key for joined-table inheritance:

from flask_sqlalchemy import Model, SQLAlchemy
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declared_attr, has_inherited_table

class IdModel(Model):
    @declared_attr
    def id(cls):
        for base in cls.__mro__[1:-1]:
            if getattr(base, '__table__', None) is not None:
                type = sa.ForeignKey(base.id)
                break
        else:
            type = sa.Integer

        return sa.Column(type, primary_key=True)

db = SQLAlchemy(model_class=IdModel)

class User(db.Model):
    name = db.Column(db.String)

class Employee(User):
    title = db.Column(db.String)
Answered By: Kıvanç Yüksel

Actually if you are very famillar with the mechanism of metaclass in python, the porblem is very simple. I give you a example in SqlAlchemy.

from sqlalchemy.ext.declarative import as_declarative, declared_attr

@as_declarative()
class Base():
    id = Column(Integer, primary_key=True, index=True)
    __name__: str
    @declared_attr
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    @property
    def url(self):
        return f'/{self.__class__.__name__.lower()}/{self.id}/'

class AnotherMetaClass(type):
    def __new__(cls, name, bases, attrs):
        pass
        # do something

class ModelMeta(Base.__class__, AnotherMetaClass):
    ...

class User(Base, metaclass=ModelMeta):
   pass

The key steps is that you should make the metaclass of Base in SqlAlchemy consistent with the metclass you design by yourself. One solution is to make a new metaclass that is the subclass of both of them.

Answered By: Louis Y ou ng

Another variant if you want metaclasses to update your classes options or methods before creating instances or set it during project initialization you can add init_subclass method to your Base.

class ClassMetod:
def __init__(self, cls):
    self._class = cls

@as_declarative()
class Base:

    @declared_attr
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    def __init_subclass__(cls):
        super().__init_subclass__()

        cls.method = ClassMetod(cls)
Answered By: MaC'kRage
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.