Opened 4 years ago

Closed 3 years ago

Last modified 17 months ago

#32508 closed Cleanup/optimization (fixed)

Raise proper exceptions instead of using "assert".

Reported by: Adam Johnson Owned by: Mateo Radman
Component: Core (Other) Version: dev
Severity: Normal Keywords:
Cc: Paolo Melchiorre Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Python's -O flag strips 'assert' statements: https://docs.python.org/3/using/cmdline.html#cmdoption-o . A naive user might enable this flag to try "make their code go faster", whilst any gain would be very marginal.

Django currently has 89 assert statements guarding against various kinds of bad data. It's also common for library or project code to use 'assert' without realizing it could be turned off.

I propose adding a system check to warn or error if the assert statement doesn't work. This can be checked with something like:

try:
    assert False
except AssertionError:
    pass
else:
    errors.append(checks.Error("Django relies on the 'assert' statement, do not disable it with 'python -O'"))

Change History (48)

comment:1 by Simon Charette, 4 years ago

An alternative to this problem could also be to replace these assert by proper exceptions instead.

comment:2 by Mariusz Felisiak, 4 years ago

Component: UncategorizedCore (Other)
Summary: Add a system check that 'assert' is not disabledRaise proper exceptions instead of using "assert".
Triage Stage: UnreviewedAccepted
Type: UncategorizedCleanup/optimization

I agree with Simon. We don't add assert to Django anymore, so I would prefer to replace these by proper exceptions (mostly TypeError and ValueError).

comment:3 by Tim Graham, 4 years ago

I agree anything user-facing should be an exception, but it might not be necessary to replace all usages. For example, there might be assert conditions in the ORM that aren't expected to be triggered unless there's some bug in Django. In this case, the assert merely helps when refactoring, etc.

comment:4 by Chris Jerdonek, 4 years ago

I agree with Tim. Before proceeding too far with this, it might be worth documenting in the coding style guidelines when assert is appropriate so you have something to judge cases by. It's useful e.g. when you know an invariant should be preserved by Django, but you don't want to regularly incur the performance hit when invoked with -O.

comment:5 by Adam Johnson, 4 years ago

Fair enough, I agree that removing such usage is probably just as valid.

comment:6 by Mariusz Felisiak, 4 years ago

I added PR for a good start.

comment:7 by Carlton Gibson, 4 years ago

It's also common for library...

Just as a data-point, DRF makes extensive use of assert for this purpose.

comment:8 by GitHub <noreply@…>, 4 years ago

In ba9a2b75:

Refs #32508 -- Raised TypeError instead of using "assert" on unsupported operations for sliced querysets.

comment:9 by Adam Johnson, 4 years ago

Just as a data-point, DRF makes extensive use of assert for this purpose.

Would you make a ticket for DRF? Or is it just too much work?

comment:10 by Carlton Gibson, 4 years ago

Maybe a system check for DRF is a good idea…

I can't quite decide. I've never run python -O. It sort of misses the point of Python IMO... — but if someone were to do it, yes. I guess a PR with it fully in place would be better than a ticket. 😀

comment:11 by Hasan Ramezani, 4 years ago

PR for changing asserts in admindoc and auth middlewares

comment:12 by Adam Johnson, 4 years ago

It sort of misses the point of Python IMO...

Sure, but that doesn't stop someone seeing their code is slow, trying it, and leaving it in place.

Maybe a system check for DRF is a good idea…

If a system check is a good idea on DRF I don't see why it wouldn't be a good idea in Django itself. Putting it in DRF it would effectively force 79.5%* of the ecosystem to not use -O, so we'd be most of the way. Even if django itself stops using assert there's an unknown amount of code in third party libraries etc. using it.

*2020 survey stat

comment:13 by Carlton Gibson, 4 years ago

If a system check is a good idea on DRF...

Yeah... if — note I said maybe (with emphasis). I'm kind of -0 on this (for here or there) — I don't think we can sustainably add system checks for every which way you might possibly shoot yourself in the foot. If using -O is a bad move, that's something that Python should warn about no?

I don't know that the issue tracker is the best place to knock this back and forth. The first four responses here seem against adding a system check, without at least doing various other things first, so I think we're a way from a consensus to add one…

comment:14 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

In a2d5ea62:

Refs #32508 -- Raised ImproperlyConfigured instead of using "assert" in middlewares.

comment:15 by Adam Johnson, 4 years ago

I don't think we can sustainably add system checks for every which way you might possibly shoot yourself in the foot.

True - I just get tired of repairing feet 😅

I don't know that the issue tracker is the best place to knock this back and forth.

Agree. Let's just stick with the current proposal to remove use of assert in prod code. I made an issue for DRF: https://github.com/encode/django-rest-framework/issues/7831

comment:16 by Hasan Ramezani, 4 years ago

I've created a PR to replace assert in django.utils

comment:17 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

In 775b796:

Refs #32508 -- Raised ValueError instead of using "assert" in lazy().

comment:18 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

In 54d91795:

Refs #32508 -- Raised ImproperlyConfigured instead of using "assert" in SessionStorage.

comment:19 by Daniyal Abbasi, 4 years ago

I have created a PR to remove usage of assert in django.core.

Next up, I would like to work on removing the usage of assert in django.db module. If anyone else is already working on it, do let me know. Thanks!

in reply to:  19 comment:20 by Mariusz Felisiak, 4 years ago

Replying to Daniyal Abbasi:

Next up, I would like to work on removing the usage of assert in django.db module.

Please take into account Tim's comment.

comment:21 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

In 474cc420:

Refs #32508 -- Raised Type/ValueError instead of using "assert" in django.core.

comment:22 by Nishant Sagar, 3 years ago

May I take this ticket and if yes can anyone suggests me the ways to proceed as I'm fairly new to this

in reply to:  22 comment:23 by Jacob Walls, 3 years ago

Replying to Nishant Sagar:

May I take this ticket and if yes can anyone suggests me the ways to proceed as I'm fairly new to this

Thanks for your interest. Daniyal handled the bulk of what was left here in PR 14173, so if you want to tackle the rest, be sure to skip the django.db module, which was the focus of that PR.

Toward getting started, this is a great time to learn about grep, e.g. grep 'assert\s' django/* -r. Here's what's left excluding django.db and the test suite itself:

django/contrib/auth/hashers.py:        assert password is not None
django/contrib/auth/hashers.py:        assert salt and '$' not in salt
django/contrib/auth/hashers.py:        assert algorithm == self.algorithm
django/contrib/auth/hashers.py:        assert algorithm == self.algorithm
django/contrib/auth/hashers.py:        assert algorithm == self.algorithm
django/contrib/auth/hashers.py:        assert algorithm == self.algorithm
django/contrib/auth/hashers.py:        assert algorithm == self.algorithm
django/contrib/auth/hashers.py:        assert password is not None
django/contrib/auth/hashers.py:        assert salt and '$' not in salt
django/contrib/auth/hashers.py:        assert algorithm == self.algorithm
django/contrib/auth/hashers.py:        assert password is not None
django/contrib/auth/hashers.py:        assert salt and '$' not in salt
django/contrib/auth/hashers.py:        assert algorithm == self.algorithm
django/contrib/auth/hashers.py:        assert salt == ''
django/contrib/auth/hashers.py:        assert encoded.startswith('sha1$$')
django/contrib/auth/hashers.py:        assert salt == ''
django/contrib/auth/hashers.py:        assert len(salt) == 2
django/contrib/auth/hashers.py:        assert hash is not None  # A platform like OpenBSD with a dummy crypt module.
django/contrib/auth/hashers.py:        assert algorithm == self.algorithm
django/contrib/auth/views.py:        assert 'uidb64' in kwargs and 'token' in kwargs
django/contrib/staticfiles/storage.py:                assert url_path.startswith(settings.STATIC_URL)
django/dispatch/dispatcher.py:            assert callable(receiver), "Signal receivers must be callable."
django/http/multipartparser.py:                assert remaining > 0, 'remaining bytes to read should never go negative'
django/test/client.py:        assert self.__len >= num_bytes, "Cannot read more than the available bytes from the HTTP incoming data."
django/test/utils.py:            assert not kwargs
django/test/utils.py:            assert not args
django/test/testcases.py:        assert not self.reset_sequences, 'reset_sequences cannot be used on TestCase instances'
django/utils/version.py:        assert len(version) == 5
django/utils/version.py:        assert version[3] in ('alpha', 'beta', 'rc', 'final')
django/utils/regex_helper.py:            assert not flags, (
django/views/decorators/debug.py:            assert isinstance(request, HttpRequest), (

The work to finish this off is to decide which ones remaining are worth changing given comments above from Tim and Chris. I suspect not many. The last one in the list could be; it's potentially user-facing and already has a nice exception message, so we can just change to TypeError. The ones in contrib.auth.hashers look like sanity checks we don't need to touch.

Version 0, edited 3 years ago by Jacob Walls (next)

in reply to:  22 comment:24 by Bal Krishna Jha, 3 years ago

Replying to Nishant Sagar:

May I take this ticket and if yes can anyone suggests me the ways to proceed as I'm fairly new to this

If you're still working on it, please let me know the progress and component you've chosen. So that I can choose other component or leave this ticket.

comment:25 by Mateo Radman, 3 years ago

Replaced assert keywords by raising proper exception with a descriptive message included.
PR
Files changed:

  • views/decorators/debug.py
  • dispatch/dispatcher.py
  • http/multipartparser.py
  • utils/regex_helper.py
  • contrib/staticfiles/storage.py
  • contrib/auth/views.py

comment:26 by Mariusz Felisiak <felisiak.mariusz@…>, 3 years ago

In 8a7ac78b:

Refs #32508 -- Raised ImproperlyConfigured/TypeError instead of using "assert" in various code.

comment:27 by Mateo Radman, 3 years ago

Hey guys, what is left to be done for this ticket?
Thanks.

in reply to:  27 comment:28 by Mariusz Felisiak, 3 years ago

Replying to Mateo Radman:

Hey guys, what is left to be done for this ticket?
Thanks.

IMO, we have only two cases that could be updated (outside of django.db.models):

  • django/db/backends/postgresql/creation.py - assert test_settings['COLLATION'] is None
  • django/test/testcases.py - assert not self.reset_sequences, 'reset_sequences cannot be used on TestCase instances'

P.S. Note that we're not all "guys" so please use gender neutral greetings, https://heyguys.cc/

comment:29 by Mateo Radman, 3 years ago

Thank you very much and apologies for my gendered greeting.
I’ll start working on replacing these two asserts.

in reply to:  29 comment:30 by Mateo Radman, 3 years ago

Replying to Mateo Radman:

Thank you very much and apologies for my gendered greeting.
I’ll start working on replacing these two asserts.

PR

comment:31 by Mariusz Felisiak <felisiak.mariusz@…>, 3 years ago

In 22314299:

Refs #32508 -- Raised ImproperlyConfigured/TypeError instead of using "assert".

comment:32 by Jacob Walls, 3 years ago

Has patch: set
Owner: changed from nobody to Daniyal Abbasi
Patch needs improvement: set
Status: newassigned

Once Daniyal's patch lands for django.db.models I think this will be resolved.

comment:33 by Mariusz Felisiak <felisiak.mariusz@…>, 3 years ago

In f479df7:

Refs #32508 -- Raised Type/ValueError instead of using "assert" in django.db.models.

Co-authored-by: Mariusz Felisiak <felisiak.mariusz@…>

comment:34 by Mariusz Felisiak, 3 years ago

Patch needs improvement: unset
Resolution: fixed
Status: assignedclosed

IMO, the other asserts are fine.

comment:35 by Chris Jerdonek, 3 years ago

I'd like to suggest that the asserts in hashers.py (listed above) also be considered. While reviewing PR #13799, I noticed there are cases where user code can encounter them.

For example, here are a couple of the asserts, in the case of PBKDF2PasswordHasher.encode():
https://github.com/django/django/blob/012f38f9594b35743e9ab231757b7b62db638323/django/contrib/auth/hashers.py#L271-L273
And here is an example in the Django docs instructing users to call this method with their own code (for the purposes of a data migration):
https://docs.djangoproject.com/en/3.2/topics/auth/passwords/#password-upgrading-without-requiring-a-login
That section also tells the user they "can modify the pattern to work with any algorithm or with a custom user model." Since Django has examples telling users to call this method with their own code, and also since this is a security-related code path, I think it would be worth switching from assert statements to more explicit argument checking.

Last edited 3 years ago by Chris Jerdonek (previous) (diff)

comment:36 by Paolo Melchiorre, 3 years ago

Cc: Paolo Melchiorre added

I agree with Chris, and I think we can reopen this issue.

in reply to:  35 comment:37 by Mariusz Felisiak, 3 years ago

Resolution: fixed
Status: closednew

OK, let's change asserts in encode() methods.

comment:38 by GitHub <noreply@…>, 3 years ago

In 83022d2:

Refs #32508 -- Raised TypeError/ValueError instead of using "assert" in encode() methods of some password hashers.

comment:39 by Mariusz Felisiak, 3 years ago

Has patch: unset
Owner: Daniyal Abbasi removed
Status: newassigned

comment:40 by Mariusz Felisiak, 3 years ago

Status: assignednew

asserts in the following methods should be changed:

  • UnsaltedSHA1PasswordHasher.encode(),
  • UnsaltedMD5PasswordHasher.encode(),
  • CryptPasswordHasher.encode(),

new tests are also required.

Last edited 3 years ago by Mariusz Felisiak (previous) (diff)

in reply to:  40 comment:41 by Mateo Radman, 3 years ago

Owner: set to Mateo Radman
Status: newassigned

Replying to Mariusz Felisiak:

asserts in the following methods should be changed:

  • UnsaltedSHA1PasswordHasher.encode(),
  • UnsaltedMD5PasswordHasher.encode(),
  • CryptPasswordHasher.encode(),

new tests are also required.

I'll do it!

in reply to:  40 comment:42 by Mateo Radman, 3 years ago

Replying to Mariusz Felisiak:

asserts in the following methods should be changed:

  • UnsaltedSHA1PasswordHasher.encode(),
  • UnsaltedMD5PasswordHasher.encode(),
  • CryptPasswordHasher.encode(),

new tests are also required.

PR

comment:43 by Jacob Walls, 3 years ago

Has patch: set

comment:44 by Mariusz Felisiak <felisiak.mariusz@…>, 3 years ago

In a7f27fca:

Refs #32508 -- Raised TypeError/ValueError instead of using "assert" in encode() methods of remaining password hashers.

comment:45 by Mariusz Felisiak, 3 years ago

Has patch: unset
Resolution: fixed
Status: assignedclosed

comment:46 by Adam Johnson, 3 years ago

Thanks all!

comment:47 by David, 17 months ago

There are still many assert statements in the codebase https://github.com/search?q=repo%3Adjango%2Fdjango+language%3APython+path%3A%2F%5Edjango%5C%2F%2F++content%3A%22+assert+%22&type=code

Are they going to stay there or is it ok to remove them?

in reply to:  47 comment:48 by Mariusz Felisiak, 17 months ago

Replying to David:

Are they going to stay there or is it ok to remove them?

We want to keep them, please check the entire discussion.

Note: See TracTickets for help on using tickets.
Back to Top