-
-
Notifications
You must be signed in to change notification settings - Fork 523
/
meta_class.py
188 lines (140 loc) · 6.03 KB
/
meta_class.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
"""
Metaclass are used to modify a class as it is being created at runtime.
This module shows how a metaclass can add database attributes and tables
to "logic-free" model classes for the developer.
"""
from abc import ABC
class ModelMeta(type):
"""Model metaclass.
By studying how SQLAlchemy and Django ORM work under the hood, we can see
a metaclass can add useful abstractions to class definitions at runtime.
That being said, this metaclass is a toy example and does not reflect
everything that happens in either framework. Check out the source code
in SQLAlchemy and Django to see what actually happens:
https://github.com/sqlalchemy/sqlalchemy
https://github.com/django/django
The main use cases for a metaclass are (A) to modify a class before
it is visible to a developer and (B) to add a class to a dynamic registry
for further automation.
Do NOT use a metaclass if a task can be done more simply with class
composition, class inheritance or functions. Simple code is the reason
why Python is attractive for 99% of users.
For more on metaclass mechanisms, visit the link below:
https://realpython.com/python-metaclasses/
"""
# Model table registry
tables = {}
def __new__(mcs, name, bases, attrs):
"""Factory for modifying the defined class at runtime.
Here are the following steps that we take:
1. Get the defined model class
2. Add a model_name attribute to it
3. Add a model_fields attribute to it
4. Add a model_table attribute to it
5. Link its model_table to a registry of model tables
6. Return the modified model class
"""
kls = super().__new__(mcs, name, bases, attrs)
# Abstract model does not have a `model_name` but a real model does.
# We will leverage this fact later on this routine
if attrs.get("__abstract__") is True:
kls.model_name = None
else:
custom_name = attrs.get("__table_name__")
default_name = kls.__name__.replace("Model", "").lower()
kls.model_name = custom_name if custom_name else default_name
# Ensure abstract and real models have fields so that
# they can be inherited
kls.model_fields = {}
# Fill model fields from the parent classes (left-to-right)
for base in bases:
kls.model_fields.update(base.model_fields)
# Fill model fields from itself
kls.model_fields.update({
field_name: field_obj
for field_name, field_obj in attrs.items()
if isinstance(field_obj, BaseField)
})
# Register a real table (a table with valid `model_name`) to
# the metaclass `table` registry. After all the tables are
# registered, the registry can be sent to a database adapter
# which uses each table to create a properly defined schema
# for the database of choice (i.e. PostgresSQL, MySQL)
if kls.model_name:
kls.model_table = ModelTable(kls.model_name, kls.model_fields)
ModelMeta.tables[kls.model_name] = kls.model_table
else:
kls.model_table = None
return kls
@property
def is_registered(cls):
"""Check if the model's name is valid and exists in the registry."""
return cls.model_name and cls.model_name in cls.tables
class ModelTable:
"""Model table."""
def __init__(self, table_name, table_fields):
self.table_name = table_name
self.table_fields = table_fields
class BaseField(ABC):
"""Base field."""
class CharField(BaseField):
"""Character field."""
class IntegerField(BaseField):
"""Integer field."""
class BaseModel(metaclass=ModelMeta):
"""Base model.
Notice how `ModelMeta` is injected at the base class. The base class
and its subclasses will be processed by the method `__new__` in the
`ModelMeta` class before being created.
In short, think of a metaclass as the creator of classes. This is
very similar to how classes are the creator of instances.
"""
__abstract__ = True # This is NOT a real table
row_id = IntegerField()
class UserModel(BaseModel):
"""User model."""
__table_name__ = "user_rocks" # This is a custom table name
username = CharField()
password = CharField()
age = CharField()
sex = CharField()
class AddressModel(BaseModel):
"""Address model."""
user_id = IntegerField()
address = CharField()
state = CharField()
zip_code = CharField()
def main():
# Real models are given a name at runtime with `ModelMeta`
assert UserModel.model_name == "user_rocks"
assert AddressModel.model_name == "address"
# Real models are given fields at runtime with `ModelMeta`
assert "row_id" in UserModel.model_fields
assert "row_id" in AddressModel.model_fields
assert "username" in UserModel.model_fields
assert "address" in AddressModel.model_fields
# Real models are registered at runtime with `ModelMeta`
assert UserModel.is_registered
assert AddressModel.is_registered
# Real models have a `ModelTable` that can be used for DB setup
assert isinstance(ModelMeta.tables[UserModel.model_name], ModelTable)
assert isinstance(ModelMeta.tables[AddressModel.model_name], ModelTable)
# Base model is given special treatment at runtime
assert not BaseModel.is_registered
assert BaseModel.model_name is None
assert BaseModel.model_table is None
# Every model is created by `ModelMeta`
assert isinstance(BaseModel, ModelMeta)
assert all(isinstance(model, ModelMeta)
for model in BaseModel.__subclasses__())
# And `ModelMeta` is created by `type`
assert isinstance(ModelMeta, type)
# And `type` is created by `type` itself
assert isinstance(type, type)
# And everything in Python is an object!
assert isinstance(BaseModel, object)
assert isinstance(ModelMeta, object)
assert isinstance(type, object)
assert isinstance(object, object)
if __name__ == "__main__":
main()