Recursively create same class during instantiation - python

I have a json config that I want to create a dict from. Because json configs are recursive, any time I see a json value that is an array I want to recursively iterate on it. However this is not doing what I want it to do.
class FieldHandler():
formfields = {}
def __init__(self, fields):
for field in fields:
options = self.get_options(field)
f = getattr(self, "create_field_for_" +
field['type'])(field, options)
self.formfields[field['name']] = f
def get_options(self, field):
options = {}
options['label'] = field['name']
options['help_text'] = field.get("help_text", None)
options['required'] = bool(field.get("required", 0))
return options
def create_field_for_string(self, field, options):
options['max_length'] = int(field.get("max_length", "20"))
return django.forms.CharField(**options)
def create_field_for_int(self, field, options):
options['max_value'] = int(field.get("max_value", "999999999"))
options['min_value'] = int(field.get("min_value", "-999999999"))
return django.forms.IntegerField(**options)
def create_field_for_array(self, field, options):
fh = FieldHandler(field['elements'])
return fh
and instantiating:
fh = FieldHandler([
{'type': 'string', 'name': 'position'},
{'type': 'array', 'name': 'calendar', 'elements': [
{'type': 'string', 'name': 'country'},
{'type': 'string', 'name': 'url'},
]},
{'type': 'int', 'name': 'maxSize'}
])
I expect to get a dict like so:
{
'position': <django.forms.fields.CharField object at 0x10b57af50>,
'calendar': <__main__.FieldHandler instance at 0x10b57c680>,
'maxSize': <django.forms.fields.IntegerField object at 0x10b58e050>,
}
Where calendar itself is expected to be:
{
'url': <django.forms.fields.CharField object at 0x10b58e150>,
'country': <django.forms.fields.CharField object at 0x10b58e0d0>
}
Instead I get:
{
'url': <django.forms.fields.CharField object at 0x10b58e150>,
'position': <django.forms.fields.CharField object at 0x10b57af50>,
'calendar': <__main__.FieldHandler instance at 0x10b57c680>,
'maxSize': <django.forms.fields.IntegerField object at 0x10b58e050>,
'country': <django.forms.fields.CharField object at 0x10b58e0d0>
}
What am I doing wrong? Why are the position and country parameters being set on my global FieldHandler?

formfields is a class attribute that is shared among all instances. Make it an instance attribute instead:
class FieldHandler():
def __init__(self, fields):
self.formfields = {}
# ...
Now, all FieldHandler instances have their own formfields, with only the "inner" calendar handler having the country and url (not position assuming that was a typo) fields.

Related

With format='multipart' in test client, data of nested dict been ignored or removed

I have a nested serializer containing, containing an Image Field in the nested serializer, the serializers are:-
class FloorPlanLocationSerializer(serializers.ModelSerializer):
class Meta:
model = FloorPlan
fields = (
'floor',
'image',
)
extra_kwargs = {'floor': {'required': False}, 'image': {'required': False}}
class LocationSerializer(FilterSerializerByOrgManaged, serializers.ModelSerializer):
floorplan = FloorPlanLocationSerializer(required=False, allow_null=True)
class Meta:
model = Location
fields = (
'id',
'organization',
'name',
'type',
'is_mobile',
'address',
'geometry',
'created',
'modified',
'floorplan',
)
read_only_fields = ('created', 'modified')
def to_representation(self, instance):
request = self.context['request']
data = super().to_representation(instance)
floorplans = instance.floorplan_set.all().order_by('-modified')
floorplan_list = []
for floorplan in floorplans:
dict_ = {
'floor': floorplan.floor,
'image': request.build_absolute_uri(floorplan.image.url),
}
floorplan_list.append(dict_)
data['floorplan'] = floorplan_list
return data
def create(self, validated_data):
floorplan_data = None
if validated_data.get('floorplan'):
floorplan_data = validated_data.pop('floorplan')
instance = self.instance or self.Meta.model(**validated_data)
with transaction.atomic():
instance.full_clean()
instance.save()
if floorplan_data:
floorplan_data['location'] = instance
floorplan_data['organization'] = instance.organization
with transaction.atomic():
fl = FloorPlan.objects.create(**floorplan_data)
fl.full_clean()
fl.save()
return instance
With this above serialzier, it works fine with DRF Browsable page, but when I try to send the data with the test client in multipart format, the nested data gets removed while send the POST request, this is how I wrote the tests:-
def test_create_location_with_floorplan_api(self):
path = reverse('geo_api:list_location')
coords = json.loads(Point(2, 23).geojson)
image = Image.new("RGB", (100, 100))
with tempfile.NamedTemporaryFile(suffix=".png", mode="w+b") as tmp_file:
image.save(tmp_file, format="png")
tmp_file.seek(0)
byio = BytesIO(tmp_file.read())
inm_file = InMemoryUploadedFile(
file=byio,
field_name="avatar",
name="testImage.png",
content_type="image/png",
size=byio.getbuffer().nbytes,
charset=None,
)
data = {
'organization': self._get_org().pk,
'name': 'test-location',
'type': 'indoor',
'is_mobile': False,
'address': 'Via del Corso, Roma, Italia',
'geometry': {'Type': 'Point', 'coordinates': [12.32,43.222]},
'floorplan': {
'floor': 12,
'image': inm_file
},
}
with self.assertNumQueries(6):
response = self.client.post(path, data, format='multipart')
self.assertEqual(response.status_code, 201)
The data doesn't come in the same format as I sent, i.e., when I try to see the data in the to_internal method this is how I receive it:-
<QueryDict: {'organization': ['f6c406e5-0602-44a7-9160-ec109ac29f4c'], 'name': ['test-location'], 'type': ['indoor'], 'is_mobile': ['False'], 'address': ['Via del Corso, Roma, Italia'], 'geometry': ['type', 'coordinates'], 'floorplan': ['floor', 'image']}>
the values of type, coordinates, floorplan are not present inside it.
How can I write a proper tests for the above case???
If you want to post form data, you need to flatten everything the same way a browser would. Maybe this gist will help, flatten_dict_for_form_data. Its quite old and could use some cleanup, but it still works.
This recursively flattens a dict, which you can then send to test client (or to live services):
def flatten_dict_for_formdata(input_dict, sep="[{i}]"):
def __flatten(value, prefix, result_dict, previous=None):
if isinstance(value, dict):
if previous == "dict":
prefix += "."
for key, v in value.items():
__flatten(v, prefix + key, result_dict, "dict")
elif isinstance(value, (list, tuple)):
for i, v in enumerate(value):
__flatten(v, prefix + sep.format(i=i), result_dict)
else:
result_dict[prefix] = value
return result_dict
return __flatten(input_dict, '', {})
>>> flatten_dict_for_formdata({
>>> "name": "Test",
>>> "location": {"lat": 1, "lng": 2},
>>> "sizes": ["S", "M", "XL"]
>>> })
>>> {
>>> "name": "Test",
>>> "location.lat": 1,
>>> "location.lng": 2,
>>> "sizes[0]": "S",
>>> "sizes[1]": "M",
>>> "sizes[2]": "XL"
>>> }

invoke dictionary fields dynamically by name

I have a function like this that allows to me to get the dictionary key like get_key(dictionary, 'path.to.key')
#staticmethod
def get_key(cont, key, default=None):
def get_key_inner(parent, keys, default):
if not keys:
return default
cur = keys.pop(0)
if cur not in parent:
return default
if not keys:
return parent[cur]
if isinstance(parent[cur], dict):
return get_key_inner(parent[cur], keys, default)
return default
return get_key_inner(cont, key.split('.'), default)
I want to implement some kind of object wrapper that would accept dictionary as a parameter and then would delegate the calls by name using the dictionary keys. Something like this
class Wrapper:
def __init__(self, dict_source):
self.source = dict_source
leon_dict = {
'name': 'Leon',
'job': {
'title': 'engineer'
}
}
leon = Wrapper(leon_dict)
assert 'engineer' == leon.job.title # the same as leon['job']['title']
Can I achieve that?
class Wrapper:
def __init__(self, dict_source):
self.source = dict_source
def __getattr__(self, key):
try:
data = self.source[key]
if isinstance(data, dict):
return Wrapper(data)
return data
except KeyError as e:
print(e)
raise AttributeError()
leon_dict = {
'name': 'Leon',
'job': {
'title': 'engineer'
}
}
leon = Wrapper(leon_dict)
print(leon.job.title)

json serialization of a list of objects of a custom class

I have a song class, which holds the attributes to a song, and it is a custom class. I also have a list of songs in a list called track list. When I try to json.dump the list, I get an error that says :
TypeError: Object of type 'Song' is not JSON serializable
How would I go about converting this list of songs to json?
Here is the additional relevant code that returns the error:
class Song:
def __init__(self, sname, sartist, coverart, albname, albartist, spotid):
self.sname = sname
self.sartist = sartist
self.coverart = coverart
self.albname = albname
self.albartist = albartist
self.spotid = spotid
tracklist = createDict(tracks) ##creates the list of songs, works fine
jsontracks = json.dumps(tracklist)
pp.pprint(jsontracks)
Thanks
I've solved this by adding an encode() method to the class:
def encode(self):
return self.__dict__
and adding some arguments to json.dumps:
jsontracks = json.dumps(tracklist, default=lambda o: o.encode(), indent=4)
This will "crawl" down your class tree (if you have any child classes) and encode every object as a json list/object automatically. This should work with just about any class and is fast to type. You may also want to control which class parameters get encoded with something like:
def encode(self):
return {'name': self.name,
'code': self.code,
'amount': self.amount,
'minimum': self.minimum,
'maximum': self.maximum}
or a little bit faster to edit (if you're lazy like me):
def encode(self):
encoded_items = ['name', 'code', 'batch_size', 'cost',
'unit', 'ingredients', 'nutrients']
return {k: v for k, v in self.__dict__.items() if k in encoded_items}
full code:
import json
class Song:
def __init__(self, sname, sartist, coverart, albname, albartist, spotid):
self.sname = sname
self.sartist = sartist
self.coverart = coverart
self.albname = albname
self.albartist = albartist
self.spotid = spotid
def encode(self):
return self.__dict__
tracklist = [
Song('Imagine', 'John Lennon', None, None, None, None),
Song('Hey Jude', 'The Beatles', None, None, None, None),
Song('(I Can\'t Get No) Satisfaction', 'The Rolling Stones', None, None, None, None),
]
jsontracks = json.dumps(tracklist, default=lambda o: o.encode(), indent=4)
print(jsontracks)
output:
[
{
"sname": "Imagine",
"sartist": "John Lennon",
"coverart": null,
"albname": null,
"albartist": null,
"spotid": null
},
{
"sname": "Hey Jude",
"sartist": "The Beatles",
"coverart": null,
"albname": null,
"albartist": null,
"spotid": null
},
{
"sname": "(I Can't Get No) Satisfaction",
"sartist": "The Rolling Stones",
"coverart": null,
"albname": null,
"albartist": null,
"spotid": null
}
]

Python MagicMock.return_value returning MagicMock instead of return_value

I have a function that verifies if a given input string is a proper GCP zone:
def validate_zone(compute, project_id, zone):
try:
zone_response = compute.zones().get(project=project_id, zone=zone).execute()
print(zone_response)
print(zone_response.return_value)
if ['status'] in zone_response:
zone_details = {
'status': zone_response['status'],
'region': zone_response['region'],
'name': zone_response['name']
}
return zone_details
else:
return "Zone {} not found for project {}".format(zone, project_id)
except HttpError as error:
print("Error calling zone {}: \n {}".format(zone, error))
I am trying to write a test to verify that but I can't mock the output of the compute method correctly.
#patch('googleapiclient.discovery')
def test_validate_zone(self, mock_response):
compute = mock_response.build(serviceName='compute', version='v1')
compute.zones().get(project_id=self.project_id, zone=self.zone).execute().return_value = {
'status': 'status',
'region': 'region',
'name': 'name'
}
zone_response = inventory.validate_zone(compute, self.project_id, self.zone)
print(zone_response)
This results in the zone_response output being a MagicMock object with its return_value being correct as developed in the test.
zone_response = MagicMock name='discovery.build().zones().get().execute()' id='139870134525456'
zone_response.return_value = {'status': 'status', 'region': 'region', 'name': 'name'}
Any ideas on what I'm doing wrong? I've been trying to write tests for this for quite a while so maybe my approach is just off.
Turns out the issue was the () on the execute method in the test. So the correct test should be:
#patch('inventory.discovery.build', serviceName='compute', version='v1')
def test_validate_zone(self, compute):
print(compute)
compute.zones().get(project_id=self.project_id, zone=self.zone).execute.return_value = {
'status': 'status',
'region': 'region',
'name': 'name'
}
zone_response = inventory.validate_zone(compute, self.project_id, self.zone)
print(zone_response)
Source can be found at: https://realpython.com/python-mock-library/#managing-a-mocks-return-value

How can I marshal a nested list of links in Flask-Restful?

I would like to marshal an object so that I get a response containing a list of links using Url, List and Nested from the Flask-Restful api.
job_link_fields = {
'href': restful.fields.Url('ep1', absolute=False),
'rel': restful.fields.Url('ep2', absolute=False)
}
job_fields = {
'name': restful.fields.String,
'links': restful.fields.List(restful.fields.Nested(job_link_fields))
}
class JobDao():
def __init__(self, id, job):
self.name = job['name']
self.links = [{'rel': 'jobs', 'id': id},
{'rel': 'jobs', 'id': id}]
class Job(restful.Resource):
#marshal_with(job_fields)
def get(self, id):
return JobDao(id, jobs[id-1])
But in the Url class, I need to specify the endpoint in the constructor which prevents me from adding differend job_link_fields in the List. How can I create a list of link?

Categories

Resources