Skip to content
This repository has been archived by the owner on Apr 10, 2023. It is now read-only.

Commit

Permalink
Remove fields.create() method
Browse files Browse the repository at this point in the history
  • Loading branch information
rossmacarthur committed Feb 15, 2020
1 parent c95b6b3 commit 2a54886
Show file tree
Hide file tree
Showing 3 changed files with 22 additions and 241 deletions.
17 changes: 0 additions & 17 deletions README.rst
Expand Up @@ -442,23 +442,6 @@ to only accept dates after 15th April 1912. Note: the ``rename`` argument only
applies to the serializing and deserializing of the data, the ``Album`` class
would still be instantiated using ``Album(released=...)``.

The ``create()`` method can be used to generate a new ``Field`` class from
arbitrary functions without having to manually subclass a ``Field``. For example
if we wanted a ``Percent`` field we would do the following.

.. code-block:: python
>>> from serde import fields, validators
>>>
>>> Percent = fields.create(
... 'Percent',
... fields.Float,
... validators=[validators.Between(0.0, 100.0)]
... )
>>>
>>> issubclass(Percent, fields.Float)
True
If these methods of creating custom ``Field`` classes are not satisfactory, you
can always subclass a ``Field`` and override the relevant methods.

Expand Down
160 changes: 15 additions & 145 deletions src/serde/fields.py
Expand Up @@ -6,7 +6,6 @@
import datetime
import re
import uuid
from functools import wraps

import isodate
from six import binary_type, integer_types, text_type
Expand Down Expand Up @@ -333,145 +332,6 @@ def validate(self, value):
pass


def _create_serialize(cls, serializers):
"""
Create a new serialize method with extra serializer functions.
"""

@wraps(serializers[0])
def serialize(self, value):
for serializer in serializers:
value = serializer(value)
value = super(cls, self).serialize(value)
return value

return serialize


def _create_deserialize(cls, deserializers):
"""
Create a new deserialize method with extra deserializer functions.
"""

@wraps(deserializers[0])
def deserialize(self, value):
value = super(cls, self).deserialize(value)
for deserializer in deserializers:
value = deserializer(value)
return value

return deserialize


def _create_normalize(cls, normalizers):
"""
Create a new normalize method with extra normalizer functions.
"""

@wraps(normalizers[0])
def normalize(self, value):
value = super(cls, self).normalize(value)
for normalizer in normalizers:
value = normalizer(value)
return value

return normalize


def _create_validate(cls, validators):
"""
Create a new validate method with extra validator functions.
"""

@wraps(validators[0])
def validate(self, value):
super(cls, self).validate(value)
for validator in validators:
validator(value)

return validate


def create(
name,
base=None,
args=None,
description=None,
serializers=None,
deserializers=None,
normalizers=None,
validators=None,
):
"""
Create a new `Field` class.
This is a convenience method for creating new Field classes from arbitrary
serializer, deserializer, normalizer, and/or validator functions.
Args:
name (str): the name of the class.
base (Field): the base field class to subclass.
args (tuple): positional arguments for the base class's ``__init__``
method.
serializers (list): a list of serializer functions taking the value to
serialize as an argument. The functions need to raise an `Exception`
if they fail. These serializer functions will be applied before the
primary serializer on this field.
deserializers (list): a list of deserializer functions taking the value
to deserialize as an argument. The functions need to raise an
`Exception` if they fail. These deserializer functions will be
applied after the primary deserializer on this field.
normalizers (list): a list of normalizer functions taking the value to
normalize as an argument. The functions need to raise an `Exception`
if they fail. These normalizer functions will be applied after the
primary normalizer on this field.
validators (list): a list of validator functions taking the value to
validate as an argument. The functions need to raise an `Exception`
if they fail.
Returns:
class: a new `Field` class.
"""
if not base:
base = Field

if not description:
description = name.lower()

doc = """\
{description}
Args:
**kwargs: keyword arguments for the `Field` constructor.
""".format(
description=description
)

field_cls = type(name, (base,), {'__doc__': doc})

if args:

def __init__(self, **kwargs): # noqa: N807
super(field_cls, self).__init__(*args, **kwargs)

__init__.__doc__ = 'Create a new `{name}`.'.format(name=name)
field_cls.__init__ = __init__

if serializers:
field_cls.serialize = _create_serialize(field_cls, serializers)

if deserializers:
field_cls.deserialize = _create_deserialize(field_cls, deserializers)

if normalizers:
field_cls.normalize = _create_normalize(field_cls, normalizers)

if validators:
field_cls.validate = _create_validate(field_cls, validators)

return field_cls


class Optional(Field):
"""
An optional field.
Expand Down Expand Up @@ -925,14 +785,25 @@ def _apply(self, stage, element):
return getattr(f, stage)(v)


def create_primitive(name, type):
def create_primitive(name, type_):
"""
Create a primitive `Field` class.
"""
description = 'This field represents the built-in `{type}` type.'.format(
type=type.__name__
doc = """\
This field represents the built-in `{type}` type.
Args:
**kwargs: keyword arguments for the `Field` constructor.
""".format(
type=type_
)
return create(name, base=Instance, args=(type,), description=description)

def __init__(self, **kwargs): # noqa: N807
Instance.__init__(self, type_, **kwargs)

__init__.__doc__ = 'Create a new `{name}`.'.format(name=name)

return type(name, (Instance,), {'__doc__': doc, '__init__': __init__})


Bool = create_primitive('Bool', bool)
Expand Down Expand Up @@ -1343,4 +1214,3 @@ def validate(self, value):
del create_from

__all__ = [name for name, obj in globals().items() if is_subclass(obj, Field)]
__all__.append('create')
86 changes: 7 additions & 79 deletions tests/test_fields.py
Expand Up @@ -40,16 +40,16 @@
_Mapping,
_resolve_to_field_instance,
_Sequence,
create,
)


Reversed = create( # noqa: N806
'Reversed',
base=Str,
serializers=[lambda x: x[::-1]],
deserializers=[lambda x: x[::-1]],
)
class Reversed(Str):
def __init__(self, **kwargs):
serializers = kwargs.setdefault('serializers', [])
deserializers = kwargs.setdefault('deserializers', [])
serializers.append(lambda x: x[::-1])
deserializers.append(lambda x: x[::-1])
super(Reversed, self).__init__(**kwargs)


def test_overridden_methods():
Expand Down Expand Up @@ -359,78 +359,6 @@ def test_validate(self):
field.validate(value)


def test_create_base():
# By default the created Field should subclass Field.
Example = create('Example') # noqa: N806
assert issubclass(Example, Field)
assert Example.__mro__[1] == Field


def test_create_str():
# You should be able to specify a different base Field.
Example = create('Example', base=Str) # noqa: N806
assert issubclass(Example, Str)
assert Example.__mro__[1] == Str


def test_create_args():
# You should be able to create a new Field, subclassing a Field that
# requires positional arguments.
Example = create('Example', base=Instance, args=(str,)) # noqa: N806
Example()

# This is what should happen if you don't give it the arguments!
Example = create('Example', base=Instance) # noqa: N806
with raises(TypeError):
Example()


def test_create_serializer_and_normalizer_and_deserializer():
# You should be able to create a new Field with extra serializers,
# normalizers, and deserializers.

def reverser(value):
return value[::-1]

Reversed = create( # noqa: N806
'Reversed',
base=Str,
serializers=[reverser],
deserializers=[reverser],
normalizers=[reverser],
)

class Example(Model):
a = Reversed()

field = Example(a='test')
assert field.a == 'tset'

field = Example.from_dict({'a': 'test'})
assert field.a == 'test'
assert field.to_dict() == {'a': 'tset'}


def test_create_validator():
# You should be able to create a Field with an arbitrary validate method.

def assert_is_not_derp(value):
if value == 'derp':
raise ValidationError("value is 'derp'")
assert value != 'derp'

NotDerp = create('NotDerp', Str, validators=[assert_is_not_derp]) # noqa: N806

class Example(Model):
a = NotDerp()

assert Example('notderp').a == 'notderp'

with raises(ValidationError) as e:
Example(a='derp')
assert e.value.messages() == {'a': "value is 'derp'"}


class TestOptional:
def test___init___basic(self):
# Construct a basic Optional and check values are set correctly.
Expand Down

0 comments on commit 2a54886

Please sign in to comment.