Skip to content

Commit 7558e71

Browse files
authored
Merge pull request #7 from AdCombo/6-fix-sqlalchemy-datalayer-create-object
fix: SqlalchemyDataLayer fails on models with __init__ statement
2 parents ed99ea4 + dc44ae9 commit 7558e71

File tree

4 files changed

+172
-1
lines changed

4 files changed

+172
-1
lines changed

flask_combo_jsonapi/resource.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from flask_combo_jsonapi.schema import compute_schema, get_relationships, get_model_field
1818
from flask_combo_jsonapi.data_layers.base import BaseDataLayer
1919
from flask_combo_jsonapi.data_layers.alchemy import SqlalchemyDataLayer
20-
from flask_combo_jsonapi.utils import JSONEncoder
20+
from flask_combo_jsonapi.utils import JSONEncoder, validate_model_init_params
2121

2222

2323
class ResourceMeta(MethodViewType):
@@ -39,6 +39,14 @@ def __new__(cls, name, bases, d):
3939
data_layer_kwargs = d["data_layer"]
4040
rv._data_layer = data_layer_cls(data_layer_kwargs)
4141

42+
if "schema" in d and "model" in data_layer_kwargs:
43+
model = data_layer_kwargs["model"]
44+
schema_fields = [get_model_field(d["schema"], key) for key in d["schema"]._declared_fields.keys()]
45+
invalid_params = validate_model_init_params(model=model, params_names=schema_fields)
46+
if invalid_params:
47+
raise Exception(f"Construction of {name} failed. Schema '{d['schema'].__name__}' has "
48+
f"fields={invalid_params} are not declare in {model.__name__} init parameters")
49+
4250
rv.decorators = (check_headers,)
4351
if "decorators" in d:
4452
rv.decorators += d["decorators"]

flask_combo_jsonapi/utils.py

+29
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import inspect
2+
13
import simplejson as json
24
from uuid import UUID
35
from datetime import datetime
@@ -17,3 +19,30 @@ def default(self, obj):
1719
elif isinstance(obj, UUID):
1820
return str(obj)
1921
return json.JSONEncoder.default(self, obj)
22+
23+
24+
def get_model_init_params_names(model):
25+
"""Retrieve all params of model init method
26+
27+
:param DeclarativeMeta model: an object from sqlalchemy
28+
:return tuple: list of init method fields names and boolean flag that init method has kwargs
29+
"""
30+
argnames, _, varkw = inspect.getfullargspec(model.__init__)[:3]
31+
if argnames:
32+
argnames.remove('self')
33+
return argnames, bool(varkw)
34+
35+
36+
def validate_model_init_params(model, params_names):
37+
"""Retrieve invalid params of model init method if it exists
38+
:param DeclarativeMeta model: an object from sqlalchemy
39+
:param list params_names: parameters names to check
40+
:return list: list of invalid fields or None
41+
"""
42+
init_args, has_kwargs = get_model_init_params_names(model)
43+
if has_kwargs:
44+
return
45+
46+
invalid_params = [name for name in params_names if name not in init_args]
47+
if invalid_params:
48+
return invalid_params

tests/test_resource.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from unittest.mock import Mock
2+
3+
import pytest
4+
from marshmallow_jsonapi import fields, Schema
5+
from sqlalchemy import Column, Integer, String
6+
from sqlalchemy.ext.declarative import declarative_base
7+
8+
from flask_combo_jsonapi import ResourceList
9+
10+
11+
@pytest.fixture(scope='module')
12+
def base():
13+
yield declarative_base()
14+
15+
16+
@pytest.fixture(scope='module')
17+
def model(base):
18+
class SampleModel(base):
19+
__tablename__ = 'model_sample'
20+
21+
id = Column(Integer, primary_key=True, index=True)
22+
key1 = Column(String)
23+
key2 = Column(String)
24+
25+
def __init__(self, key1):
26+
pass
27+
28+
yield SampleModel
29+
30+
31+
@pytest.fixture(scope='module')
32+
def schema_for_model(model):
33+
class SampleSchema(Schema):
34+
class Meta:
35+
model = model
36+
37+
id = fields.Integer()
38+
key1 = fields.String()
39+
key2 = fields.String()
40+
41+
yield SampleSchema
42+
43+
44+
def test_resource_meta_init(model, schema_for_model):
45+
expected_fields = ['id', 'key2']
46+
raised_ex = None
47+
try:
48+
class SampleResourceList(ResourceList):
49+
schema = schema_for_model
50+
methods = ['GET', 'POST']
51+
data_layer = {
52+
'session': Mock(),
53+
'model': model,
54+
}
55+
except Exception as ex:
56+
raised_ex = ex
57+
58+
assert raised_ex
59+
message = f"Construction of SampleResourceList failed. Schema '{schema_for_model.__name__}' " \
60+
f"has fields={expected_fields}"
61+
assert message in str(raised_ex)

tests/test_utils.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import pytest
2+
from sqlalchemy import Column, Integer, String
3+
from sqlalchemy.ext.declarative import declarative_base
4+
5+
from flask_combo_jsonapi.utils import get_model_init_params_names, validate_model_init_params
6+
7+
8+
@pytest.fixture(scope='module')
9+
def base():
10+
yield declarative_base()
11+
12+
13+
@pytest.fixture(scope='module')
14+
def model_without_init(base):
15+
class ModelWithoutInit(base):
16+
__tablename__ = 'model_without_init'
17+
18+
id = Column(Integer, primary_key=True, index=True)
19+
key1 = Column(String)
20+
key2 = Column(String)
21+
22+
yield ModelWithoutInit
23+
24+
25+
@pytest.fixture(scope='module')
26+
def model_with_kwargs_in_init(base):
27+
class ModelWithKwargsInit(base):
28+
__tablename__ = 'model_with_kwargs_init'
29+
30+
id = Column(Integer, primary_key=True, index=True)
31+
key1 = Column(String)
32+
key2 = Column(String)
33+
34+
def __init__(self, key1=0, **kwargs):
35+
pass
36+
37+
yield ModelWithKwargsInit
38+
39+
40+
@pytest.fixture(scope='module')
41+
def model_with_positional_args_init(base):
42+
class ModelWithPositionalArgsInit(base):
43+
__tablename__ = 'model_with_positional_args_init'
44+
45+
id = Column(Integer, primary_key=True, index=True)
46+
key1 = Column(String)
47+
key2 = Column(String)
48+
49+
def __init__(self, key1, key2=0):
50+
pass
51+
52+
yield ModelWithPositionalArgsInit
53+
54+
55+
def test_get_model_init_params_names(model_without_init, model_with_kwargs_in_init,
56+
model_with_positional_args_init):
57+
args, has_kwargs = get_model_init_params_names(model_without_init)
58+
assert ([], True) == (args, has_kwargs)
59+
60+
args, has_kwargs = get_model_init_params_names(model_with_kwargs_in_init)
61+
assert (['key1'], True) == (args, has_kwargs)
62+
63+
args, has_kwargs = get_model_init_params_names(model_with_positional_args_init)
64+
assert (['key1', 'key2'], False) == (args, has_kwargs)
65+
66+
67+
def test_validate_model_init_params(model_with_kwargs_in_init, model_with_positional_args_init):
68+
schema_attrs = ['id', 'key1', 'key2']
69+
invalid_params = validate_model_init_params(model_with_kwargs_in_init, schema_attrs)
70+
assert invalid_params is None
71+
72+
invalid_params = validate_model_init_params(model_with_positional_args_init, schema_attrs)
73+
assert invalid_params == ['id']

0 commit comments

Comments
 (0)