Skip to content

Commit 19ce2da

Browse files
authored
Merge pull request #72 from n0nSmoker/62-support-for-serializing-return-values-of-methods-that-have-default-arguments
Include callables with default args in serialization
2 parents 3d56d57 + 2b9ff61 commit 19ce2da

5 files changed

Lines changed: 97 additions & 20 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ class Point(Base, SerializerMixin):
416416
position = Column(Geometry('POINT'))
417417
```
418418

419-
Unfortunately you can not access formats or tzinfo in that functions.
419+
Unfortunately you cannot access formats or tzinfo in that functions.
420420
I'll implement this logic later if any of users needs it.
421421

422422

@@ -517,7 +517,7 @@ But `serialize_only = ('model', '-model.id',)` will return `model` field without
517517
Do not forget to add **comma** at the end of one element tuples, it is trivial,
518518
but a lot of developers forget about it:
519519
```python
520-
serialize_only = ('some_field',) # <--- Thats right!
520+
serialize_only = ('some_field',) # <--- That's right!
521521
serialize_only = ('some_field') # <--- WRONG it is actually not a tuple
522522

523523
```
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
class Base:
22
def __call__(self, value) -> str:
3-
raise NotImplementedError(f"Method should implement serialization logic for{value}")
3+
raise NotImplementedError(f"Method should implement serialization logic for {value}")

sqlalchemy_serializer/serializer.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def to_dict(
8686
exclude_values=None,
8787
max_serialization_depth=None,
8888
serialize_columns=None,
89-
):
89+
) -> dict:
9090
"""Returns SQLAlchemy model's data in JSON compatible format
9191
9292
For details about datetime formats follow:
@@ -123,7 +123,7 @@ def to_dict(
123123
),
124124
serialize_columns=serialize_columns or self.serialize_columns,
125125
)
126-
return s(self, only=only, extend=rules)
126+
return s(self, only=only, extend=rules) # type: ignore
127127

128128

129129
Options = namedtuple(
@@ -133,7 +133,7 @@ def to_dict(
133133

134134

135135
class Serializer:
136-
# Types that do nod need any serialization logic
136+
# Types that do not need any serialization logic
137137
atomic_types = (
138138
int,
139139
str,
@@ -224,16 +224,28 @@ def init_callbacks(self):
224224
@staticmethod
225225
def is_valid_callable(func) -> bool:
226226
"""Determines objects that should be called before serialization"""
227-
if callable(func):
228-
i = inspect.getfullargspec(func)
229-
if (
230-
i.args == ["self"]
231-
and isinstance(func, MethodType)
232-
and not any([i.varargs, i.varkw])
233-
):
234-
return True
235-
return not any([i.args, i.varargs, i.varkw])
236-
return False
227+
if not callable(func):
228+
return False
229+
230+
i = inspect.getfullargspec(func)
231+
232+
# Check for varargs/varkw
233+
# (methods with *args or **kwargs are not callable without args)
234+
if any([i.varargs, i.varkw]):
235+
return False
236+
237+
# For methods: check if all args except 'self' have defaults
238+
if isinstance(func, MethodType):
239+
if not i.args or i.args[0] != "self":
240+
return False
241+
# All args except self must have defaults
242+
args_count = len(i.args) - 1
243+
else:
244+
# For functions: all args must have defaults
245+
args_count = len(i.args) if i.args else 0
246+
247+
defaults_count = len(i.defaults) if i.defaults else 0
248+
return args_count == defaults_count
237249

238250
def is_forkable(self, value):
239251
"""Determines if object should be processed in a separate serializer"""
@@ -273,7 +285,7 @@ def serialize(self, value, key=_UNSPECIFIED):
273285
return self.apply_callback(value=value)
274286

275287
except IsNotSerializable:
276-
logger.warning("Can not serialize type:%s", get_type(value))
288+
logger.warning("Cannot serialize type:%s", get_type(value))
277289

278290
logger.debug("Skip value:%s", value)
279291
return _UNSPECIFIED

tests/test_flat_model.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from .models import DATE, DATETIME, MONEY, TIME, FlatModel
1+
import sqlalchemy as sa
2+
3+
from .models import DATE, DATETIME, MONEY, TIME, Base, FlatModel, SerializerMixin
24

35

46
def test_no_defaults_no_rules(get_instance):
@@ -345,3 +347,66 @@ def test_rules_for_nested_dicts_and_lists(get_instance):
345347

346348
assert "prop" in data
347349
assert data["prop"] == i.prop
350+
351+
352+
def test_method_with_default_argument():
353+
"""Test that methods with default arguments are serialized"""
354+
355+
class ModelWithDefaultMethod(Base, SerializerMixin):
356+
__tablename__ = "model_with_default_method"
357+
serialize_only = ()
358+
serialize_rules = ()
359+
360+
id = sa.Column(sa.Integer, primary_key=True)
361+
string = sa.Column(sa.String(256), default="test")
362+
363+
def is_liked(self, user=None):
364+
return user is not None
365+
366+
i = ModelWithDefaultMethod(id=1, string="test")
367+
data = i.to_dict(rules=("is_liked",))
368+
369+
assert "is_liked" in data
370+
assert data["is_liked"] is False # Called without args, user=None
371+
372+
373+
def test_method_with_multiple_defaults():
374+
"""Test method with multiple default arguments"""
375+
376+
class ModelWithMultiDefaults(Base, SerializerMixin):
377+
__tablename__ = "model_with_multi_defaults"
378+
serialize_only = ()
379+
serialize_rules = ()
380+
381+
id = sa.Column(sa.Integer, primary_key=True)
382+
string = sa.Column(sa.String(256), default="test")
383+
384+
def compute(self, a=1, b=2):
385+
return a + b
386+
387+
i = ModelWithMultiDefaults(id=1, string="test")
388+
data = i.to_dict(rules=("compute",))
389+
390+
assert "compute" in data
391+
assert data["compute"] == 3 # 1 + 2
392+
393+
394+
def test_method_with_required_arg_rejected():
395+
"""Test that methods with required args are still rejected"""
396+
397+
class ModelWithRequiredArg(Base, SerializerMixin):
398+
__tablename__ = "model_with_required_arg"
399+
serialize_only = ()
400+
serialize_rules = ()
401+
402+
id = sa.Column(sa.Integer, primary_key=True)
403+
string = sa.Column(sa.String(256), default="test")
404+
405+
def method_required(self, user): # No default
406+
return user
407+
408+
i = ModelWithRequiredArg(id=1, string="test")
409+
data = i.to_dict(rules=("method_required",))
410+
411+
# Should not be serialized (not callable without args)
412+
assert "method_required" not in data

tests/test_schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,9 @@ def check_rule(text: str, tree: Tree) -> Tree:
240240
"""
241241
rule = Rule(text)
242242
for k in rule.keys:
243-
tree = tree.get(k)
243+
tree = tree.get(k) # type: ignore
244244
if tree is None:
245-
raise NoNodeException(f"Can not find key:{k} in tree:{tree}")
245+
raise NoNodeException(f"Cannot find key:{k} in tree:{tree}")
246246
if rule.is_negative:
247247
assert tree.to_exclude
248248
else:

0 commit comments

Comments
 (0)