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

Fixed #25370 - Helpful exception message for lamda #13582

Closed
wants to merge 6 commits into from

Conversation

craigiansmith
Copy link
Contributor

Lambda expressions can't be serialized. Make the exception message more
helpful when a lamda is used as an argument to a model field.

Lambda expressions can't be serialized. Make the exception message more
helpful when it is attempted.
Copy link
Member

@felixxm felixxm left a comment

Choose a reason for hiding this comment

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

@craigiansmith Thanks for this patch 👍

Comment on lines 204 to 206
msg = 'During serialization of the field'
e.args = ('{} {} the following blew up: {}'.format(msg, self.value, e.args[0]), ) + e.args[1:]
raise e
Copy link
Member

Choose a reason for hiding this comment

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

I don't see a reason for modifying a source exception, you can use:

raise ValueError(...) from e

Also blew up is not a appropriate wording and a new exception is not more informative because it refers to the field class ... the field <django.db.models.fields.CharField> instead of <app_label>.<model_name>.<field_name>, maybe:

raise ValueError('Error during %s serializing: %s' % (field, e)) from e

I don't have a quick answer how to get a field path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@felixxm thanks for the feedback. I have changed the error message as you suggested. But I have kept the .format notation instead of %s, if that should also change, please let me know. I will look into how to get the field path before I update with a new commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@felixxm self.value shows the field path in the <app label>.<model name>.<field name> form. I have tested this by starting a django project using the amended source code and attempting to makemigrations for an app with a model that has a field that uses a lambda expression.

I have had trouble getting the test to verify this behavior though, since when I try to import a model from migrations.models and serialize it, the field is treated as a DeferredAttribute instead of a model field and so doesn't get handled by the ModelFieldSerializer. When I tested in a new project I was able to get the desired behavior by changing a field on a model that already had migrations, so the only thing that had to be serialized was the field which I amended to use a lambda expression as its default. At this stage, I don't see how to achieve that in a test. Any suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By making a custom field the test error message shows the desired field path.

Copy link
Member

Choose a reason for hiding this comment

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

No it's not, without this patch it raises, e.g.

ValueError: Cannot serialize function: lambda

with this patch:

ValueError: Error during <django.db.models.fields.IntegerField> serializing: Cannot serialize function: lambda

It's not more descriptive, IMO. You can have hundreds of IntegerFields in dozens of apps. I would expect:

ValueError: Error during 'test_app.models.MyModel.field_1.default' serializing: Cannot serialize function: lambda

I don't see much value in this change if it's not feasible to get <app label>.<model name>.<field name>.<parameter> or at least <app label>.<model name>.<field name>.

Maybe we should fix this in FunctionTypeSerializer, it should be doable to get at least test_app.models.MyModel from __qualname__ and __module__ 🕵️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The FunctionTypeSerializer has the same issue, if the field isn't part of a model.

I need to figure out how to add the UnserializableField to a migrated model, within a test. So that the field is a member of a model, and that the model appears as though it is already migrated, and that the only change is to add the UnserializableField. Any ideas on how to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@felixxm the latest commit seems to have gotten the desired result.

Copy link
Member

Choose a reason for hiding this comment

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

I'm sorry but it's still the same 🤷 . You can manipulate with tests to create an unreachable state where you will get an expected message but that's not the correct solution.

Please check an attached project and try to run:

$> python manage.py makemigrations

it raises:

ValueError: Error during <django.db.models.fields.IntegerField> serializing: Cannot serialize function: lambda

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @felixxm, you were right, the error message can be constructed in the FunctionTypeSerializer - but I had trouble getting the field name, any ideas on that one? BTW thanks for the welcome aboard message :bowtie:

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for updates 👍

... but I had trouble getting the field name, any ideas on that one?

Unfortunately not, moreover I'm afraid that we will not be able to get <app label>.<model name>.<field name> or even <app label>.<model name> in a reliable way. We serialize field from django.db.models not a model attribute, that's why it's complicated or even not feasible. Each approach doesn't work in some cases, e.g. constructing messages in the FunctionTypeSerializer will not work for lambdas defined in the module:

Error during serializing test_one.models.<lambda>:  ...

or imported from other modules:

ValueError: Error during serializing test_one.utils.<lambda>: ...

I think we should close this as wontfix 😞

- Changed error message for lambda serialization
- Confirmed that error message shows field module path
@@ -252,9 +252,10 @@ def test_serialize_constants(self):
self.assertSerializedEqual(False)

def test_trying_to_serialize_a_lambda_in_field_argument_should_raise_helpful_message(self):
Copy link
Member

Choose a reason for hiding this comment

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

A test name over 79 characters seems too verbose to me.

Maybe: test_serialize_field_with_lambda ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion. I will update it.

- Change test name to make it more succinct
- Use a custom field class to demonstrate correct field path in test
try:
return self.serialize_deconstructed(path, args, kwargs)
except ValueError as e:
raise ValueError('Error during {} serializing: {}'.format(self.value, e)) from e
Copy link
Member

Choose a reason for hiding this comment

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

Guidance is to try and avoid format. Do you think this could be more readable written as an f string or % formatting?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, you're right. It's better to use an f string.

def test_serialize_field_with_lambda(self):
field = UnserializableField()

error = 'Cannot serialize function: lambda'
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion, write this as one string in full?

msg = (
    "Error during...."
    " ....."
)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By using an f string it was easy to fit it all on one line without exceeding the character limit.

- Use f string instead of string.format
- Use a single string for test message
Test shows that the error message states app_label.model.field
Construct error message in FunctionTypeSerializer instead of
ModelFieldSerializer. Remove migration for the associated test.
Rename the model used to test this.
@felixxm
Copy link
Member

felixxm commented Nov 12, 2020

@craigiansmith Thanks for your efforts! 🥇 Unfortunately, IMO it's not doable.

Closing per ticket.

@felixxm felixxm closed this Nov 12, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants