question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Dropped coverage on Django 1.11

See original GitHub issue

Tracking 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:closed
  • Created 6 years ago
  • Comments:11 (9 by maintainers)

github_iconTop GitHub Comments

1reaction
jleclanchecommented, Jun 4, 2017

Wee! We figured it out together on Gitter.

Card.stripesource_ptr is, on Django 1.10, a ForwardManyToOneDescriptor. On Django 1.11 however, it is a ForwardOneToOneDescriptor 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:

if issubclass(type(orig_accessor), (ReverseOneToOneDescriptor, ForwardManyToOneDescriptor])):

I will submit a pull request to django-polymorphic.

1reaction
twidicommented, Jun 4, 2017

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)

Read more comments on GitHub >

github_iconTop Results From Across the Web

Django 1.11 release notes
Welcome to Django 1.11! These release notes cover the new features, as well as some backwards incompatible changes you'll want to be aware...
Read more >
Django Template Coverage.py Plugin - GitHub
A plugin for coverage.py to measure Django template execution - GitHub ... v3.0.0 — 2022-12-06. Dropped support for Python 2.7, Python 3.6, and...
Read more >
Changelog — Django Compressor 4.1 documentation
With this change the function compressor.cache.get_offline_manifest_filename() has been removed. You can now use the new file storage compressor.storage.
Read more >
django-coverage-plugin - PyPI
Supported Python versions: 2.7, 3.4, 3.5, 3.6, 3.7 and 3.8. Supported Django versions: 1.8, 1.11, 2.0, 2.1, 2.2 and 3.0. Supported coverage.py ...
Read more >
Milestone 3] Upgrade Python 2 to 3 and Django 1.11 to 2.2 ...
I DROPPED django-nose from requirements and use the standard unittest library, with coverage import still in manage.py to maintain existing ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found