Below is my python marshmallow serializer which I'm using in my SQLAlchemy-based application.
import simplejson
from marshmallow import Schema
class MySerializer(Schema):
class Meta:
json_module = simplejson
fields = ('field1', 'field2', 'field3')
field3 = fields.Method('get_field3')
def get_field3(self, this_obj):
return "Hello"
Here is how I call this serializer:
my_argument = 1
items=MySerializer(documents=list_of_my_objects, many=True).data
Now, I want to change the serializer such that it takes an additional argument (I will pass in my_argument) and if the value of that argument is 1, return "Goodbye" in place of "Hello". How can I do that?
In particular, I don't know how I can pass in an argument to this marshamallow Schema such that it is available from insideget_field3. I know I need to override the __init__() method. But I'm not sure what it should look like. I tried the following, but it didn't work:
def __init__(self, documents, many, my_arg):
self.my_arg = my_arg
super(Schema, self).__init__(documents, many=many)
def get_field3(self, this_obj):
self.my_arg == 1:
return "Goodbye"
else:
return "Hello"
This was the stack-trace I got:
my_project/my_models/serializers.pyc in __init__(self, documents, many, my_arg)
---> 25 super(Schema, self).__init__(documents, many=many)
my_virtualenv/lib/python2.7/site-packages/marshmallow/schema.pyc in __init__(self, obj, extra, only, exclude, prefix, strict, many, skip_missing, context)
--> 273 self._update_fields(self.obj, many=many)
my_virtualenv/lib/python2.7/site-packages/marshmallow/schema.pyc in _update_fields(self, obj, many)
--> 636 ret = self.__filter_fields(field_names, obj, many=many)
my_virtualenv/lib/python2.7/site-packages/marshmallow/schema.pyc in __filter_fields(self, field_names, obj, many)
--> 683 attribute_type = type(obj_dict[key])
TypeError: list indices must be integers, not str
Just pop added arg from kwargs to avoid any errors (you can pass any kwargs to method redefined this way):
class MySerializer(Schema):
# ...
def __init__(self, *args, **kwargs):
self.my_arg = kwargs.pop('my_arg') if 'my_arg' in kwargs else None
super(Schema, self).__init__(*args, **kwargs)
now you can pass custom kwarg (keyword argument, not positional) to create objects:
MySerializer(documents=list_of_my_objects, many=True, my_arg=1)
Related
I'm facing an interesting problem in Python with Django.
I think just by putting my code you will get what I'm trying to do.
views.py:
def ingredients(request):
objects = Ingredient.objects.all()[:50]
return render(request, 'template.html', {'objects': objects}
models.py:
class Ingredient(models.Model):
stock_by = models.IntegerField(null=False)
unit = ""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
unit_type = {
1: 'Units',
2: 'Kilograms',
3: 'Litters'
}
self.unit = unit_type[IntegerField.to_python(self.cost_by)]
Error:
TypeError at /Ingredient/
to_python() missing 1 required positional argument: 'value'
(Value is None).
init.py (Django framework class):
class IntegerField(field):
def to_python(self, value):
if value is None:
return value
try:
return int(value)
except (TypeError, ValueError):
raise exceptions.ValidationError(
self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
I think it's clear what I'm trying to achieve. Just a String attribute which will take the name of the unit value (represented by integers in db).
to_python is an instance method you have to call it from the instance, not the class.
IntegerField().to_python(self.cost_by)
I'm unit testing a view and I am attempting to patch the .data property on my serializer but it looks like it behaves differently when the many=True kwarg is passed to the serializer constructor and thus not properly patching. Here is a generalized example of my code.
# myapp/serializers.py
class MySerializer(serializers.Serializer):
some_field = serializers.CharField()
# myapp/views.py
class MyView(View):
def get(self, request):
# ..stuff
some_data = []
serializer = MySerializer(some_data, many=True)
print(type(serializer)) # <class 'rest_framework.serializers.ListSerializer'>
print(type(serializer.data)) # <class 'rest_framework.utils.serializer_helpers.ReturnList'>
return Response({"data": seralizer.data, status=200})
# in tests
def test_view_case_one(mocker):
# setup other mocks
serialized_data = mocker.patch("myapp.views.MySerializer.data", new_callable=mocker.PropertyMock)
# invoke view
response = MyView().get(fake_request)
# run assertions
serialized_data.assert_called_once() # this says it's never called
Earlier I had ran into issues attempting to patch rest_framework.serializers.ListSerializer.data. Must of been a typo. Reattempted and was able to successfully patch. Given the case many=True recreates the serializer as a ListSerializer I simply needed to patch the property on the underlying class.
serialized_data = mocker.patch(
"rest_framework.serializers.ListSerializer.data",
new_callable=mocker.PropertyMock
)
Edit: A more in depth answer
When many=True is used the __new__ method on BaseSerializer grabs you class and constructs a ListSerializer from it and that is why my object showed up as a ListSerializer. Since we are actually receiving a ListSerializer instead of our defined class the patch is not applied to ListSerializer.data method. The relevant parts of the source code for BaseSerializer is below
class BaseSerializer(Field):
def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create
# `ListSerializer` classes instead when `many=True` is set.
if kwargs.pop('many', False):
return cls.many_init(*args, **kwargs)
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
#classmethod
def many_init(cls, *args, **kwargs):
"""
This method implements the creation of a `ListSerializer` parent
class when `many=True` is used. You can customize it if you need to
control which keyword arguments are passed to the parent, and
which are passed to the child.
Note that we're over-cautious in passing most arguments to both parent
and child classes in order to try to cover the general case. If you're
overriding this method you'll probably want something much simpler, eg:
#classmethod
def many_init(cls, *args, **kwargs):
kwargs['child'] = cls()
return CustomListSerializer(*args, **kwargs)
"""
allow_empty = kwargs.pop('allow_empty', None)
child_serializer = cls(*args, **kwargs)
list_kwargs = {
'child': child_serializer,
}
if allow_empty is not None:
list_kwargs['allow_empty'] = allow_empty
list_kwargs.update({
key: value for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS
})
meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
return list_serializer_class(*args, **list_kwargs)
Having already looked at this answer, I'm having trouble getting my custom WTForm validator to accept another arg.
How my code is structures is that I have a form_create() method that is called via the following:
form = other_file.form_create(
form='page_settings',
data=data,
all_applications=all_applications,
)
all_applications being a dict I need to index through for one of my validators. form_create looks like the following:
def get_form(form=None, *args, **kwargs):
class Form(wtf.Form):
if form == 'page_settings':
my_host_name = HostName('Hostname', validators=[wtf.validators.DataRequired(), validate_hostname, validate_config])
I want validate_config to use data from the field and from all_applications, but even after putting it in HostName's init method as such:
class HostName(wtf.StringField):
def __init__(self, *args, **kwargs):
kwargs.setdefault('validators', [
wtf.validators.DataRequired(),
])
all_applications = kwargs.setdefault('all_applications')
super(HostNameField, self).__init__(*args, **kwargs)
But I keep getting errors like ‘Form’ object has no attribute ‘all_applications’
The custom validator in question looks like this:
def validate_config(form, field, all_applications):
if (some logic here is true):
raise wtf.ValidationError(
'some error string here'
)
Any ideas as to how I can get a custom validator to accept another container like all_applications?
You can set all_applications as an attribute of the field when the form is being initialised, then access it via the field in the validator.
def validate_config(form, field):
if field.data and field.all_applications is not None:
if field.data not in field.all_applications:
raise ValidationError('Invalid config')
class F(Form):
foo = StringField(validators=[validate_config])
def __init__(self, *args, **kwargs):
all_applications = kwargs.pop('all_applications', None)
super().__init__(*args, **kwargs)
# Make all_applications an attribute of field foo
self.foo.all_applications = all_applications
>>> from webob.multidict import MultiDict
>>> f = F(formdata=MultiDict(foo='a'), all_applications={'a': 1})
>>> f.validate()
True
>>> f = F(formdata=MultiDict(foo='b'), all_applications={'a': 1})
>>> f.validate()
False
>>> f.errors
{'foo': ['Invalid config']}
I have the following SerializerField:
class TimestampField(Field):
def to_representation(self, value):
if not value:
return ''
return value.timestamp()
And I use it like this in my serializer:
class ArticlePhotobookSerializer(ModelSerializer):
delivery_date_from = TimestampField()
delivery_date_to = TimestampField()
Now the getter delivery_date_to can return None, which I want to transform into an empty string using the to_representation method. however, when I use the Serializer to parse this None value, it doesn't even enter the to_representation method and immediately returns None. What should I change to also use the method to_representation for None?
By default serializer's to_representation method skip fields with None value (see source).
You can write mixin class to override default to_representation:
class ToReprMixin(object):
def to_representation(self, instance):
ret = OrderedDict()
fields = [field for field in self.fields.values() if not field.write_only]
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
ret[field.field_name] = field.to_representation(attribute)
return ret
and use it in your serializers:
class ArticlePhotobookSerializer(ToReprMixin, ModelSerializer):
...
If you would like to change the result of to_representation when there is no instance (not exactly the same problem as you had, but matches the question title), to_representation will not even be called in DRF v3. One can change the result by subclassing the get_initial method:
def get_initial(self):
"""
Return a value to use when the field is being returned as a primitive
value, without any object instance.
"""
if callable(self.initial):
return self.initial()
return self.initial
Heres an example:
def get_initial(self) -> dict:
return {'display_name': 'moocows'}
Here we use the context as initial representation:
def get_initial(self) -> dict:
return self.context
Is there a way to have any #property definitions passed through to a json serializer when serializing a Django model class?
example:
class FooBar(object.Model)
name = models.CharField(...)
#property
def foo(self):
return "My name is %s" %self.name
Want to serialize to:
[{
'name' : 'Test User',
'foo' : 'My name is Test User',
},]
You can extend Django's serializers without /too/ much work. Here's a custom serializer that takes a queryset and a list of attributes (fields or not), and returns JSON.
from StringIO import StringIO
from django.core.serializers.json import Serializer
class MySerializer(Serializer):
def serialize(self, queryset, list_of_attributes, **options):
self.options = options
self.stream = options.get("stream", StringIO())
self.start_serialization()
for obj in queryset:
self.start_object(obj)
for field in list_of_attributes:
self.handle_field(obj, field)
self.end_object(obj)
self.end_serialization()
return self.getvalue()
def handle_field(self, obj, field):
self._current[field] = getattr(obj, field)
Usage:
>>> MySerializer().serialize(MyModel.objects.all(), ["field1", "property2", ...])
Of course, this is probably more work than just writing your own simpler JSON serializer, but maybe not more work than your own XML serializer (you'd have to redefine "handle_field" to match the XML case in addition to changing the base class to do that).
The solution worked well that is proposed by M. Rafay Aleem and Wtower, but it's duplicated lot of code. Here is an improvment:
from django.core.serializers.base import Serializer as BaseSerializer
from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.json import Serializer as JsonSerializer
class ExtBaseSerializer(BaseSerializer):
def serialize_property(self, obj):
model = type(obj)
for field in self.selected_fields:
if hasattr(model, field) and type(getattr(model, field)) == property:
self.handle_prop(obj, field)
def handle_prop(self, obj, field):
self._current[field] = getattr(obj, field)
def end_object(self, obj):
self.serialize_property(obj)
super(ExtBaseSerializer, self).end_object(obj)
class ExtPythonSerializer(ExtBaseSerializer, PythonSerializer):
pass
class ExtJsonSerializer(ExtPythonSerializer, JsonSerializer):
pass
How to use it:
ExtJsonSerializer().serialize(MyModel.objects.all(), fields=['field_name_1', 'property_1' ...])
This is a combination of M. Rafay Aleem and Wtowers answer and caots.
This is DRY and lets you only specify the extra props instead of all fields and props as in caots version.
from django.core.serializers.json import Serializer as JsonSerializer
from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.base import Serializer as BaseSerializer
class ExtBaseSerializer(BaseSerializer):
def serialize(self, queryset, **options):
self.selected_props = options.pop('props')
return super(ExtBaseSerializer, self).serialize(queryset, **options)
def serialize_property(self, obj):
model = type(obj)
for field in self.selected_props:
if hasattr(model, field) and type(getattr(model, field)) == property:
self.handle_prop(obj, field)
def handle_prop(self, obj, field):
self._current[field] = getattr(obj, field)
def end_object(self, obj):
self.serialize_property(obj)
super(ExtBaseSerializer, self).end_object(obj)
class ExtPythonSerializer(ExtBaseSerializer, PythonSerializer):
pass
class ExtJsonSerializer(ExtPythonSerializer, JsonSerializer):
pass
How to use it:
ExtJsonSerializer().serialize(MyModel.objects.all(), props=['property_1', ...])
Things have changed a bit since 2010, so the answer of #user85461 seems to no longer be working with Django 1.8 and Python 3.4. This is an updated answer with what seems to work for me.
from django.core.serializers.base import Serializer as BaseSerializer
from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.json import Serializer as JsonSerializer
from django.utils import six
class ExtBaseSerializer(BaseSerializer):
""" Abstract serializer class; everything is the same as Django's base except from the marked lines """
def serialize(self, queryset, **options):
self.options = options
self.stream = options.pop('stream', six.StringIO())
self.selected_fields = options.pop('fields', None)
self.selected_props = options.pop('props', None) # added this
self.use_natural_keys = options.pop('use_natural_keys', False)
self.use_natural_foreign_keys = options.pop('use_natural_foreign_keys', False)
self.use_natural_primary_keys = options.pop('use_natural_primary_keys', False)
self.start_serialization()
self.first = True
for obj in queryset:
self.start_object(obj)
concrete_model = obj._meta.concrete_model
for field in concrete_model._meta.local_fields:
if field.serialize:
if field.rel is None:
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_field(obj, field)
else:
if self.selected_fields is None or field.attname[:-3] in self.selected_fields:
self.handle_fk_field(obj, field)
for field in concrete_model._meta.many_to_many:
if field.serialize:
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_m2m_field(obj, field)
# added this loop
if self.selected_props:
for field in self.selected_props:
self.handle_prop(obj, field)
self.end_object(obj)
if self.first:
self.first = False
self.end_serialization()
return self.getvalue()
# added this function
def handle_prop(self, obj, field):
self._current[field] = getattr(obj, field)
class ExtPythonSerializer(ExtBaseSerializer, PythonSerializer):
pass
class ExtJsonSerializer(ExtPythonSerializer, JsonSerializer):
pass
Usage:
>>> ExtJsonSerializer().serialize(MyModel.objects.all(), fields=['myfield', ...], props=['myprop', ...])
You can get all of the properties of a class using some black magic:
def list_class_properties(cls):
return [k for k,v in cls.__dict__.iteritems() if type(v) is property]
For example:
>>> class Foo:
#property
def bar(self):
return "bar"
>>> list_class_properties(Foo)
['bar']
Then you can build the dictionary and serialize it from there.