Dropped coverage on Django 1.11
See original GitHub issueTracking issue for Django 1.11 dropped coverage.
On Django 1.11, coverage has dropped in one very specific place: Card.remove()
:
def remove(self):
"""Removes a card from this customer's account."""
try:
self._api_delete()
except InvalidRequestError as exc:
if "No such source:" in str(exc) or "No such customer:" in str(exc):
# The exception was thrown because the stripe customer or card was already
# deleted on the stripe side, ignore the exception
pass
else:
# The exception was raised for another reason, re-raise it
six.reraise(*sys.exc_info())
try:
self.delete()
except StripeCard.DoesNotExist:
# The card has already been deleted (potentially during the API call)
pass
In the following method, the final except
is never raised. Now, this is supposedly harmless, but we want to understand why before we greenlight Django 1.11.
This try/except was added by yours truly in commit 942ced2e0d19eeda923e188034e87082bccc7022. That commit added the following test which forces the DoesNotExist to trigger:
@patch("stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER))
def test_remove_already_deleted_card(self, customer_retrieve_mock):
stripe_card = Card._api_create(customer=self.customer, source=FAKE_CARD["id"])
Card.sync_from_stripe_data(stripe_card)
self.assertEqual(self.customer.sources.count(), 1)
card_object = self.customer.sources.first()
Card.objects.filter(stripe_id=stripe_card["id"]).delete()
self.assertEqual(self.customer.sources.count(), 0)
card_object.remove()
self.assertEqual(self.customer.sources.count(), 0)
This test can be reduced to the following, which fails on Django 1.10 and passes on Django 1.11:
def test_django111_behaviour(self):
args = {
"stripe_id": "test_django111",
"customer_id": Customer.objects.first().id,
"exp_month": 0,
"exp_year": 0,
}
card = Card.objects.create(**args)
card2 = Card.objects.get(stripe_id=args["stripe_id"])
card.delete()
card2.delete()
And here is the traceback:
Traceback (most recent call last):
File "/home/adys/src/git/hsreplaynet/dj-stripe/tests/test_card.py", line 122, in test_django111_behaviour
card2.delete()
File "/home/adys/src/git/hsreplaynet/dj-stripe/.tox/py36-django110-bjson/lib/python3.6/site-packages/django/db/models/base.py", line 957, in delete
collector.collect([self], keep_parents=keep_parents)
File "/home/adys/src/git/hsreplaynet/dj-stripe/.tox/py36-django110-bjson/lib/python3.6/site-packages/django/db/models/deletion.py", line 207, in collect
parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
File "/home/adys/src/git/hsreplaynet/dj-stripe/.tox/py36-django110-bjson/lib/python3.6/site-packages/django/db/models/deletion.py", line 207, in <listcomp>
parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
File "/home/adys/src/git/hsreplaynet/dj-stripe/.tox/py36-django110-bjson/lib/python3.6/site-packages/polymorphic/models.py", line 161, in accessor_function
attr = model.base_objects.get(pk=self.pk)
File "/home/adys/src/git/hsreplaynet/dj-stripe/.tox/py36-django110-bjson/lib/python3.6/site-packages/django/db/models/manager.py", line 85, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/home/adys/src/git/hsreplaynet/dj-stripe/.tox/py36-django110-bjson/lib/python3.6/site-packages/django/db/models/query.py", line 385, in get
self.model._meta.object_name
djstripe.stripe_objects.DoesNotExist: StripeSource matching query does not exist.
Now, it’s worth noting that raising DoesNotExist on a delete() is not normal behaviour. Easy enough to test with a random django model:
>>> Foo.objects.create()
<Foo: e15c448b-4be4-40f7-a11a-bf82988dfa43>
>>> a = _
>>> b = Foo.objects.get(id=a.id)
>>> a.delete()
(1, {'foo.Foo': 1})
>>> b.delete()
(0, {'foo.Foo': 0})
Indeed we see in the traceback that the DoesNotExist is raised not in the delete itself, but in the collector that gathers related models before performing the delete.
The Model.delete method in Django has not changed since 1.10, and for that matter neither has the Collector.delete method. The error happens in that last method in the list comprehension parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
. This accesses the name
attribute, which is created as a property on lines 161 and 179 of polymorphic/models.py.
This jumps into django/db/models/manager.py which hasn’t changed since 1.10 either. And finally ends in the get() method of query.py which… hasn’t changed either.
Some debugging lets me see the values of self, clone, args and kwargs. Django 1.10:
<QuerySet [<Card: <brand=, last4=, exp_month=0, exp_year=0, stripe_id=test_django111>>]> <QuerySet [<Card: <brand=, last4=, exp_month=0, exp_year=0, stripe_id=test_django111>>]> () {'stripe_id': 'test_django111'}
<QuerySet [<StripeSource: <stripe_id=test_django111>>]> <QuerySet [<StripeSource: <stripe_id=test_django111>>]> () {'pk': 1750}
<QuerySet []> <QuerySet []> () {'pk': 1750}
Django 1.11:
<PolymorphicQuerySet [<Card: <brand=, last4=, exp_month=0, exp_year=0, stripe_id=test_django111>>]> <PolymorphicQuerySet [<Card: <brand=, last4=, exp_month=0, exp_year=0, stripe_id=test_django111>>]> () {'stripe_id': 'test_django111'}
Okay, so on Django 1.10 it’s called three times, and once it gets to the last call it errors out. On Django 1.11 it’s only called once, because self
is not a QuerySet but a PolymorphicQuerySet.
Digging further, this confusion happens in PolymorphicManager.get_queryset(). Both in Django 1.10 and Django 1.11, self.queryset_class
is PolymorphicQuerySet
, and yet, the following line returns different things:
qs = self.queryset_class(self.model, using=self._db, hints=self._hints)
# Django 1.10 example:
self=<polymorphic.managers.PolymorphicManager object at 0x7f00ca11fe48>, <class 'polymorphic.query.PolymorphicQuerySet'>(self.model=<class 'djstripe.models.Card'>, using=self._db=None, hints=self._hints={})
-> <QuerySet []>
# Django 1.11 example:
self=<polymorphic.managers.PolymorphicManager object at 0x7fe85f955c88>, <class 'polymorphic.query.PolymorphicQuerySet'>(self.model=<class 'djstripe.models.Card'>, using=self._db=None, hints=self._hints={})
-> <PolymorphicQuerySet []>
Phew, okay, so we can reduce the failure to the following:
from polymorphic.managers import PolymorphicQuerySet
p = PolymorphicQuerySet(Card, using=None, hints={})
print(p)
# Django 1.10: <QuerySet []>
# Django 1.11: <PolymorphicQuerySet []>
Well, this is where I’m stumped. None of django’s manager.py
changed in 1.10->1.11. And yet, this changed. Any ideas?
Issue Analytics
- State:
- Created 6 years ago
- Comments:11 (9 by maintainers)
Top GitHub Comments
Wee! We figured it out together on Gitter.
Card.stripesource_ptr
is, on Django 1.10, aForwardManyToOneDescriptor
. On Django 1.11 however, it is aForwardOneToOneDescriptor
which was added in django/django@38575b007a722d6af510ea46d46393a4cda9ca29 and is a subclass of the former.That means that this if type(…) check falls through.
Signs point to the if check having to be replaced with the following:
I will submit a pull request to django-polymorphic.
It seems that they adapted the code following a change in django 1.11, related to the queryset iteration: https://github.com/django-polymorphic/django-polymorphic/commit/dbad7bd40dfdb63801095bdaf94509cd51c6c5cf
(not sure if really related, though! I was looking for important changes in this release)