Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ class Point(Base, SerializerMixin):
position = Column(Geometry('POINT'))
```

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


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

```
Expand Down
2 changes: 1 addition & 1 deletion sqlalchemy_serializer/lib/serializable/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class Base:
def __call__(self, value) -> str:
raise NotImplementedError(f"Method should implement serialization logic for{value}")
raise NotImplementedError(f"Method should implement serialization logic for {value}")
40 changes: 26 additions & 14 deletions sqlalchemy_serializer/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def to_dict(
exclude_values=None,
max_serialization_depth=None,
serialize_columns=None,
):
) -> dict:
"""Returns SQLAlchemy model's data in JSON compatible format

For details about datetime formats follow:
Expand Down Expand Up @@ -123,7 +123,7 @@ def to_dict(
),
serialize_columns=serialize_columns or self.serialize_columns,
)
return s(self, only=only, extend=rules)
return s(self, only=only, extend=rules) # type: ignore


Options = namedtuple(
Expand All @@ -133,7 +133,7 @@ def to_dict(


class Serializer:
# Types that do nod need any serialization logic
# Types that do not need any serialization logic
atomic_types = (
int,
str,
Expand Down Expand Up @@ -224,16 +224,28 @@ def init_callbacks(self):
@staticmethod
def is_valid_callable(func) -> bool:
"""Determines objects that should be called before serialization"""
if callable(func):
i = inspect.getfullargspec(func)
if (
i.args == ["self"]
and isinstance(func, MethodType)
and not any([i.varargs, i.varkw])
):
return True
return not any([i.args, i.varargs, i.varkw])
return False
if not callable(func):
return False

i = inspect.getfullargspec(func)

# Check for varargs/varkw
# (methods with *args or **kwargs are not callable without args)
if any([i.varargs, i.varkw]):
return False

# For methods: check if all args except 'self' have defaults
if isinstance(func, MethodType):
if not i.args or i.args[0] != "self":
return False
# All args except self must have defaults
args_count = len(i.args) - 1
else:
# For functions: all args must have defaults
args_count = len(i.args) if i.args else 0

defaults_count = len(i.defaults) if i.defaults else 0
return args_count == defaults_count

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

except IsNotSerializable:
logger.warning("Can not serialize type:%s", get_type(value))
logger.warning("Cannot serialize type:%s", get_type(value))

logger.debug("Skip value:%s", value)
return _UNSPECIFIED
Expand Down
67 changes: 66 additions & 1 deletion tests/test_flat_model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from .models import DATE, DATETIME, MONEY, TIME, FlatModel
import sqlalchemy as sa

from .models import DATE, DATETIME, MONEY, TIME, Base, FlatModel, SerializerMixin


def test_no_defaults_no_rules(get_instance):
Expand Down Expand Up @@ -345,3 +347,66 @@ def test_rules_for_nested_dicts_and_lists(get_instance):

assert "prop" in data
assert data["prop"] == i.prop


def test_method_with_default_argument():
"""Test that methods with default arguments are serialized"""

class ModelWithDefaultMethod(Base, SerializerMixin):
__tablename__ = "model_with_default_method"
serialize_only = ()
serialize_rules = ()

id = sa.Column(sa.Integer, primary_key=True)
string = sa.Column(sa.String(256), default="test")

def is_liked(self, user=None):
return user is not None

i = ModelWithDefaultMethod(id=1, string="test")
data = i.to_dict(rules=("is_liked",))

assert "is_liked" in data
assert data["is_liked"] is False # Called without args, user=None


def test_method_with_multiple_defaults():
"""Test method with multiple default arguments"""

class ModelWithMultiDefaults(Base, SerializerMixin):
__tablename__ = "model_with_multi_defaults"
serialize_only = ()
serialize_rules = ()

id = sa.Column(sa.Integer, primary_key=True)
string = sa.Column(sa.String(256), default="test")

def compute(self, a=1, b=2):
return a + b

i = ModelWithMultiDefaults(id=1, string="test")
data = i.to_dict(rules=("compute",))

assert "compute" in data
assert data["compute"] == 3 # 1 + 2


def test_method_with_required_arg_rejected():
"""Test that methods with required args are still rejected"""

class ModelWithRequiredArg(Base, SerializerMixin):
__tablename__ = "model_with_required_arg"
serialize_only = ()
serialize_rules = ()

id = sa.Column(sa.Integer, primary_key=True)
string = sa.Column(sa.String(256), default="test")

def method_required(self, user): # No default
return user

i = ModelWithRequiredArg(id=1, string="test")
data = i.to_dict(rules=("method_required",))

# Should not be serialized (not callable without args)
assert "method_required" not in data
4 changes: 2 additions & 2 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,9 @@ def check_rule(text: str, tree: Tree) -> Tree:
"""
rule = Rule(text)
for k in rule.keys:
tree = tree.get(k)
tree = tree.get(k) # type: ignore
if tree is None:
raise NoNodeException(f"Can not find key:{k} in tree:{tree}")
raise NoNodeException(f"Cannot find key:{k} in tree:{tree}")
if rule.is_negative:
assert tree.to_exclude
else:
Expand Down