Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add lazy loading serializer support #24

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

tfranzel
Copy link
Contributor

@tfranzel tfranzel commented Apr 6, 2020

This is one approach of supporting recursive serializers through lazy loading string classes #23

the reason the loading is located in _get_serializer_from_model_or_instance is that if it is placed in __init__ the same circular import issue occurs, because at that point in time python has not completely parsed the poly serializer classes yet.

  • (+) minimal changes
  • (+) behaves identical if classes are used instead of strings.
  • (-) accessing model_serializer_mapping by hand will not automatically load the class. However, if you use strings you are certainly aware of that and getting a string shouldn't be surprising.

originally I intended to propose a second approach, where model_serializer_mapping is automagically loaded and can be directly accessed through the attribute. As it turns out it was a lot more complicated, required deeper changes and broke some tests. So this PR is a lot more practical, understandable and gets the job done the same way basically. what do you think?

@codecov
Copy link

codecov bot commented Apr 6, 2020

Codecov Report

Merging #24 into master will increase coverage by 0.51%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##           master      #24      +/-   ##
==========================================
+ Coverage   93.10%   93.61%   +0.51%     
==========================================
  Files           1        1              
  Lines          87       94       +7     
==========================================
+ Hits           81       88       +7     
  Misses          6        6              

Copy link
Owner

@denisorehovsky denisorehovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

return self.model_serializer_mapping[klass]
serializer = self.model_serializer_mapping[klass]
if isinstance(serializer, str):
serializer = import_string(serializer)(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be better to import serializers in init method (somewhere here https://github.com/apirobot/django-rest-polymorphic/blob/master/rest_polymorphic/serializers.py#L39)? Because now we instantiate serializers in init method. Also, in that case, we wouldn't need to create serializer_args and serializer_kwargs fields. We will have something like this:

if isinstance(serializer, str):
 serializer = import_string(serializer)(*args, **kwargs)
elif callable(serializer):
  serializer = serializer(*args, **kwargs)

What do you think?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I haven't seen your PR's description first. I will take a look at this PR later

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try to use importlib.import_module? I'm looking at this code https://github.com/rsinger86/drf-flex-fields/blob/master/rest_flex_fields/serializers.py#L94, and if you can see, they import serializers in init method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

django.utils.module_loading.import_string is a convenience wrapper around importlib.import_module. effects are identical and flex-fields basically reimplements import_string in _import_serializer_class.

Unfortunatelly, I was not able to find tests in flex-fields that actually use that feature. I suspect it might break on situations like the example below.

The thing is it can work in __init__ for simpler cases but it breaks in the really interesting ones. Consider this example:

class ZSerializer(PolymorhicSerializer)
	model_serializer_mapping = {
		ModelX: 'XSerializer',
		ModelY: 'YSerializer',
	}

class XSerializer(Serializer):
	# some fields

class YSerializer(Serializer):
	# other fields
	z = ZSerializer()

Once you start using a nested field like z = ZSerializer(), the __init__ approach breaks down. You must init the ZSerializer with () there. That means ZSerializer inits inside the definition of YSerializer. When it inits it tries to load the string for YSerializer, but loading YSerializer cannot succeed as we just went into a cyclic import.

Basically the culprit is the ZSerializer(). As this is a DRF idiom there is no way around it and as a result "transitive self-referential" imports cannot happen in serializer constructors.

Also, rearranging the classes into separate files will not solve the issue.

Not sure if that is something that can happen with how flex-fields are supposed to be used. I have never used it.

Is that logic comprehensible or did I miss something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apirobot i know you are pretty pressed for time, but did you get a chance to think about this? thanks!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tfranzel Sorry that it took so long to respond. Could you please test this change?
Screenshot from 2020-04-26 15-52-48

            if isinstance(serializer, str):
                pieces = serializer.split('.')
                class_name = pieces.pop()
                module = importlib.import_module('.'.join(pieces))
                serializer = getattr(module, class_name)

When I added this code and then ran your test, it passed.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tfranzel If this change won't work as well, then probably, the only way to get this working is by pushing what you committed.

Also, I don't wanna cause any bugs. So, maybe this feature doesn't worth it?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to create serializers like these:
Screenshot from 2020-04-26 16-13-38

And I didn't receive an error with this change. Am I doing something wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apirobot thank you for taking the time! my test only covers basic functionality and not the feature i outlined in the comment above. i'll construct a test for that as well and test both versions.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. Sounds good. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants