Skip to content

Commit 179018c

Browse files
committed
chore: Include callables with default args in serialization
1 parent 3d56d57 commit 179018c

2 files changed

Lines changed: 89 additions & 12 deletions

File tree

sqlalchemy_serializer/serializer.py

Lines changed: 23 additions & 11 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:
@@ -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"""

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

0 commit comments

Comments
 (0)