For example: I want the metaclass to find all fields that start with "any_" and create new fields with different prefixes.
I try like this:
class MyModelMeta(type):
def __new__(mcs, name, bases, dic):
obj = super().__new__(mcs, name, bases, dic)
for key in filter(lambda x: x.startswith("any_"), dic.keys()):
setattr(obj, f"first_{key[4:]}", dic[key])
setattr(obj, f"second_{key[4:]}", dic[key])
delattr(obj, key)
return obj
class MyModel(Base, metaclass=MyModelMeta):
__tablename__ = "my_model"
id = sa.Column(sa.Integer, primary_key=True)
any_some = sa.Column(sa.Integer, nullable=False)
And I get:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
For combining custom metaclasses, you have to create a derived metaclass which inherits from all of them.
When one already has two metaclasses from other projects, say, abc.ABCMeta and enum.EnumMeta, it is possible to create a dynamic metaclass by just doing something like DerivedMeta = type("derived", (Meta1, Meta2), {})
and use that as the metaclass.
In this case, however, you want to modify the behavior of the metaclass used by SQLAlchemy - therefore, it makes more sense to simply inherit from it, instead of type
.
But also, when you call super().__new__
the SQLAlchemy metaclass will make all the work it needs with the fields, creating the derived objects and internal annotations used by SQLAlchemy. This means you have to do whatever modifications you want to fields-to-be-created before calling super().__new__
That said, something along this might work:
...
from copy import copy
# Use the type of SQLAlchemy's 'Base': its metaclass, instead of type:
class MyModelMeta(type(Base)):
def __new__(mcs, name, bases, dic):
# use an explicit `list` to copy the keys here so we can change the namespace
# while iterating on it
for key in filter(lambda x: x.startswith("any_"), list(dic.keys())):
# change the namespace before it is passed on to SQLAlchemy new
field_name = key.removeprefix("any_") # str.removeprefix is relatively new and thus little known, but the "obvious way" instead of "key[4:]" in this case
dic[f"first_{field_name}"] = dic[key]
# a SQLAlchemy Field entry will be an object - you can't simply re-use the same instance in two fields.
# Maybe "copy.copy" won't work straightforward here, so you will need another
# way to clone the field.
dic[f"second_{field_name}"] = copy(dic[key])
del dic[key]
# also, when dealing with metaclasses what is created here
# although it is an "obj", of course, is _the_ resulting class - it is
# more readable to call it "cls"
cls = super().__new__(mcs, name, bases, dic)
return cls
As stated in the comments, you may need more adjustments there - copy(SQLALchemyfield)
may not work - but the metaclass part should be resolved with this.