Skip to content

Commit 1109a49

Browse files
committed
fix: metaclass bug
1 parent f944e31 commit 1109a49

File tree

9 files changed

+126
-72
lines changed

9 files changed

+126
-72
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.1.23
2+
current_version = 0.1.24
33
commit = True
44
tag = True
55
parse = (?P<major>\d+)\.(?P<feat>\d+)\.(?P<patch>\d+)

easy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra"""
22

3-
__version__ = "0.1.23"
3+
__version__ = "0.1.24"
44

55
from easy.main import EasyAPI
66

easy/controller/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import logging
22

3-
from easy.controller.meta import CrudAPI, CrudApiMetaclass
3+
from easy.controller.meta import CrudApiMetaclass
44

55
logger = logging.getLogger(__name__)
66

77

8-
class CrudAPIController(CrudAPI, metaclass=CrudApiMetaclass):
8+
class CrudAPIController(metaclass=CrudApiMetaclass):
99
"""
1010
Base APIController for auto creating CRUD APIs, configurable via Meta class
1111
APIs auto generated:

easy/controller/meta.py

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import json
22
import logging
3+
import re
34
import uuid
45
from abc import ABCMeta
5-
from typing import Any, List, Optional, Tuple, Type, Union
6+
from collections import ChainMap
7+
from typing import Any, List, Match, Optional, Tuple, Type, Union
68

79
from django.db import models
810
from django.http import HttpRequest
@@ -27,16 +29,29 @@ class APIControllerBase(ControllerBase):
2729
class CrudAPI(CrudModel):
2830
# Never add type note to service, it will cause injection error
2931
def __init__(self, service=None): # type: ignore
32+
# Critical to set __Meta
33+
self.service = service
34+
if self.service:
35+
self.model = self.service.model
36+
_meta = getattr(self, "Meta", None)
37+
if self.model and _meta:
38+
setattr(
39+
self.model,
40+
"__Meta",
41+
{
42+
"model_exclude": getattr(_meta, "model_exclude", None),
43+
"model_fields": getattr(_meta, "model_fields", "__all__"),
44+
"model_recursive": getattr(_meta, "model_recursive", False),
45+
"model_join": getattr(_meta, "model_join", True),
46+
"sensitive_fields": getattr(
47+
_meta, "model_sensitive_fields", ["password", "token"]
48+
),
49+
},
50+
)
3051
if not service:
3152
self.service = BaseService(model=self.model)
32-
else:
33-
self.service = service
3453
super().__init__(model=self.model)
3554

36-
# Critical to set Meta
37-
if hasattr(self, "Meta"):
38-
self.model.Meta = self.Meta # type: ignore
39-
4055
# Define Controller APIs for auto generation
4156
async def get_obj(self, request: HttpRequest, id: int) -> Any:
4257
"""
@@ -73,28 +88,11 @@ async def get_objs(self, request: HttpRequest, filters: str = None) -> Any:
7388
return await self.service.get_objs(**json.loads(filters))
7489
return await self.service.get_objs()
7590

76-
# async def bulk_create_objs(self, request):
77-
# """
78-
# POST /bulk_create
79-
# Create multiple Object
80-
# """
81-
# return await self.service.bulk_create_objs()
82-
#
83-
# async def recover_obj(self, request):
84-
# """
85-
# PATCH /{id}/recover
86-
# Recover one Object
87-
# """
88-
# return await self.service.recover_obj()
89-
#
90-
9191

9292
class CrudApiMetaclass(ABCMeta):
9393
def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any:
9494
# Get configs from Meta
95-
temp_cls: Type = super(CrudApiMetaclass, mcs).__new__(
96-
mcs, name, (object,), attrs
97-
)
95+
temp_cls: Type = super().__new__(mcs, name, (object,), attrs)
9896
temp_opts: ModelOptions = ModelOptions(getattr(temp_cls, "Meta", None))
9997
opts_model: Optional[Type[models.Model]] = temp_opts.model
10098
opts_fields_exclude: Optional[str] = temp_opts.model_exclude
@@ -105,17 +103,18 @@ def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any:
105103
Union[str, List[str]]
106104
] = temp_opts.sensitive_fields
107105

108-
base_cls_attrs = {
109-
"get_obj": http_get("/{id}", summary="Get a single object")(
110-
copy_func(CrudAPI.get_obj) # type: ignore
111-
),
112-
"del_obj": http_delete("/{id}", summary="Delete a single object")(
113-
copy_func(CrudAPI.del_obj) # type: ignore
114-
),
115-
"get_all": http_get("/", summary="Get multiple objects")(
116-
copy_func(CrudAPI.get_objs) # type: ignore
117-
),
118-
}
106+
def is_private_attrs(attr_name: str) -> Optional[Match[str]]:
107+
return re.match(r"^__[^\d\W]\w*\Z__$", attr_name, re.UNICODE)
108+
109+
parent_attrs = ChainMap(
110+
*[attrs]
111+
+ [
112+
{k: v for (k, v) in vars(base).items() if not (is_private_attrs(k))}
113+
for base in bases
114+
]
115+
)
116+
base_cls_attrs: dict = {}
117+
base_cls_attrs.update(parent_attrs)
119118

120119
if opts_model:
121120

@@ -159,35 +158,60 @@ async def patch_obj( # type: ignore
159158
f"{opts_model.__name__}__AutoSchema({str(uuid.uuid4())[:4]})"
160159
)
161160

162-
setattr(CrudAPI, "add_obj", classmethod(add_obj))
163-
setattr(CrudAPI, "patch_obj", classmethod(patch_obj))
161+
setattr(CrudAPI, "patch_obj", patch_obj)
162+
setattr(CrudAPI, "add_obj", add_obj)
164163

165164
base_cls_attrs.update(
166165
{
167-
"patch_obj": http_patch("/{id}", summary="Patch a single object")(
166+
"patch_obj_api": http_patch(
167+
"/{id}", summary="Patch a single object"
168+
)(
168169
copy_func(CrudAPI.patch_obj) # type: ignore
169170
),
170-
"add_obj": http_put("/", summary="Create")(
171+
"add_obj_api": http_put("/", summary="Create")(
171172
copy_func(CrudAPI.add_obj) # type: ignore
172173
),
173174
}
174175
)
175176

176-
new_base: Type = type.__new__(
177-
type, name, (APIControllerBase, CrudAPI), base_cls_attrs
177+
base_cls_attrs.update(
178+
{
179+
"get_obj_api": http_get("/{id}", summary="Get a single object")(
180+
copy_func(CrudAPI.get_obj) # type: ignore
181+
),
182+
"del_obj_api": http_delete("/{id}", summary="Delete a single object")(
183+
copy_func(CrudAPI.del_obj) # type: ignore
184+
),
185+
"get_objs_api": http_get("/", summary="Get multiple objects")(
186+
copy_func(CrudAPI.get_objs) # type: ignore
187+
),
188+
}
178189
)
179-
new_cls: Type = super(CrudApiMetaclass, mcs).__new__(
180-
mcs, name, (new_base,), attrs
190+
191+
new_cls: Type = super().__new__(
192+
mcs,
193+
name,
194+
(
195+
APIControllerBase,
196+
CrudAPI,
197+
),
198+
base_cls_attrs,
181199
)
182200

183201
if opts_model:
184-
if hasattr(opts_model, "Meta"):
185-
setattr(opts_model.Meta, "model_exclude", opts_fields_exclude)
186-
setattr(opts_model.Meta, "model_fields", opts_fields)
187-
setattr(opts_model.Meta, "model_recursive", opts_recursive)
188-
setattr(opts_model.Meta, "model_join", opts_join)
189-
setattr(opts_model.Meta, "sensitive_fields", opts_sensitive_fields)
202+
setattr(
203+
opts_model,
204+
"__Meta",
205+
{
206+
"model_exclude": opts_fields_exclude,
207+
"model_fields": opts_fields,
208+
"model_recursive": opts_recursive,
209+
"model_join": opts_join,
210+
"sensitive_fields": opts_sensitive_fields,
211+
},
212+
)
190213
setattr(new_cls, "model", opts_model)
214+
191215
return new_cls
192216

193217

easy/domain/orm.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,6 @@ def __init__(self, model: Type[models.Model]):
1919
if isinstance(_field, models.ManyToManyField)
2020
)
2121

22-
# @property
23-
# def model_join(self):
24-
# """If configured to retrieve m2m by join"""
25-
# if hasattr(self.model, "Meta"):
26-
# return getattr(self.model.Meta, "model_join", True)
27-
# else:
28-
# return True
29-
3022
def _separate_payload(self, payload: Dict) -> Tuple[Dict, Dict]:
3123
m2m_fields = {}
3224
local_fields = {}

easy/domain/serializers.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ def serialize_foreign_key(
7373
except Exception as exc: # pragma: no cover
7474
logger.error(f"serialize_foreign_key error - {obj}", exc_info=exc)
7575
return {field.name: None}
76-
if hasattr(obj, "Meta") and getattr(obj.Meta, "model_recursive", False):
76+
if hasattr(obj, "__Meta") and getattr(obj, "__Meta").get(
77+
"model_recursive", False
78+
):
7779
return {
7880
field.name: self.serialize_model_instance(related_instance, referrers)
7981
}
@@ -92,7 +94,9 @@ def serialize_many_relationship(
9294
for k, v in obj._prefetched_objects_cache.items(): # type: ignore
9395
field_name = k if hasattr(obj, k) else k + "_set"
9496
if v:
95-
if hasattr(obj, "Meta") and getattr(obj.Meta, "model_join", True):
97+
if hasattr(obj, "__Meta") and getattr(obj, "__Meta").get(
98+
"model_join", True
99+
):
96100
out[field_name] = self.serialize_queryset(v, referrers + (obj,))
97101
else:
98102
out[field_name] = [o.pk for o in v]
@@ -110,8 +114,8 @@ def serialize_value_field(obj: models.Model, field: Any) -> Dict[Any, Any]:
110114
sensitive_list: List = [
111115
"password",
112116
]
113-
if hasattr(obj, "Meta"):
114-
sensitive_fields = getattr(obj.Meta, "sensitive_fields", None)
117+
if hasattr(obj, "__Meta"):
118+
sensitive_fields = getattr(obj, "__Meta").get("sensitive_fields", None)
115119
if sensitive_fields:
116120
sensitive_list.extend(sensitive_fields)
117121
sensitive_list = list(set(sensitive_list))

tests/demo_app/controllers.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class AutoGenCrudAPIController(CrudAPIController):
2626

2727
def __init__(self, service: EventService):
2828
super().__init__(service)
29-
self.service = service
3029

3130
class Meta:
3231
model = Event
@@ -35,11 +34,30 @@ class Meta:
3534

3635

3736
@api_controller("unittest", permissions=[BaseApiPermission])
38-
class RecursiveAPIController(AutoGenCrudAPIController):
37+
class RecursiveAPIController(CrudAPIController):
3938
"""
4039
For unit testings of no recursive configuration
4140
"""
4241

42+
def __init__(self, service: EventService):
43+
super().__init__(service)
44+
45+
class Meta:
46+
model = Event
47+
model_fields = "__all__"
48+
model_join = True
49+
model_recursive = True
50+
51+
52+
@api_controller("unittest", permissions=[BaseApiPermission])
53+
class InheritedRecursiveAPIController(AutoGenCrudAPIController):
54+
"""
55+
For unit testings of no recursive configuration
56+
"""
57+
58+
def __init__(self, service: EventService):
59+
super().__init__(service)
60+
4361
class Meta:
4462
model = Event
4563
model_fields = "__all__"
@@ -55,7 +73,6 @@ class AutoGenCrudNoJoinAPIController(CrudAPIController):
5573

5674
def __init__(self, service: EventService):
5775
super().__init__(service)
58-
self.service = service
5976

6077
class Meta:
6178
model = Event
@@ -83,7 +100,6 @@ class EasyCrudAPIController(CrudAPIController):
83100

84101
def __init__(self, service: EventService):
85102
super().__init__(service)
86-
self.service = service
87103

88104
class Meta:
89105
model = Event

tests/demo_app/test_async_api_permissions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,12 @@ async def test_perm_auto_apis_delete(self, transactional_db, easy_api_client):
124124

125125
# Super users
126126
client = easy_api_client(AutoGenCrudAPIController, is_superuser=True)
127-
await client.delete(
127+
response = await client.delete(
128128
f"/{event.id}",
129129
)
130130

131+
assert response.status_code == 200
132+
131133
response = await client.get(
132134
f"/{event.id}",
133135
)

tests/demo_app/test_async_auto_crud_apis.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
AutoGenCrudNoJoinAPIController,
1111
AutoGenCrudSomeFieldsAPIController,
1212
EventSchema,
13+
InheritedRecursiveAPIController,
1314
RecursiveAPIController,
1415
)
1516
from tests.demo_app.models import Category, Client, Event, Type
@@ -58,7 +59,7 @@ async def test_crud_default_get_all(self, transactional_db, easy_api_client):
5859
event_schema = json.loads(EventSchema.from_orm(event).json())
5960
assert event_schema["start_date"] == data[0]["start_date"]
6061

61-
# Recursive = False
62+
# Recursive = True
6263
client = easy_api_client(RecursiveAPIController)
6364
response = await client.get(
6465
"/", query=dict(maximum=100, filters=json.dumps(dict(id__gte=1)))
@@ -67,6 +68,20 @@ async def test_crud_default_get_all(self, transactional_db, easy_api_client):
6768

6869
data = response.json().get("data")
6970
assert data[0]["title"] == "AsyncAdminAPIEvent_get_all"
71+
72+
assert data[0]["type"]["id"] == type.id
73+
assert data[0]["category"]["status"] == 1
74+
75+
# Recursive = True, inherited class
76+
client = easy_api_client(InheritedRecursiveAPIController)
77+
response = await client.get(
78+
"/", query=dict(maximum=100, filters=json.dumps(dict(id__gte=1)))
79+
)
80+
assert response.status_code == 200
81+
82+
data = response.json().get("data")
83+
assert data[0]["title"] == "AsyncAdminAPIEvent_get_all"
84+
7085
assert data[0]["type"]["id"] == type.id
7186
assert data[0]["category"]["status"] == 1
7287

@@ -155,6 +170,7 @@ async def test_crud_default_create(self, transactional_db, easy_api_client):
155170
response = await client.get(
156171
f"/{event_id}",
157172
)
173+
158174
assert response.status_code == 200
159175
assert response.json().get("data")["title"] == "AsyncAdminAPIEvent_create"
160176

0 commit comments

Comments
 (0)