Repository: stripe/stripe-python
Issue: cannot unset a coupon

If I create a customer, and then attach an invalid coupon via `customer.coupon = "invalid"` I cannot unset this coupon code by setting `.coupon` to `None` as suggested in the documentation.

e.g.,

``` python
customer.coupon = "invalid"
customer.save()
InvalidRequestError: No such coupon: invalid
customer.coupon = ""
...
.coupon = None to delete the property

customer.coupon = None
customer.save()
InvalidRequestError: You passed an empty string for 'coupon'. We assume empty values are an attempt to unset a parameter; however 'coupon' cannot be unset. You should remove 'coupon' from your request or supply a non-empty value
```


Changed files:
stripe/test/test_resources.py, stripe/resource.py

diff --git a/stripe/resource.py b/stripe/resource.py
index 14c9f930..ceb4d6a1 100644
--- a/stripe/resource.py
+++ b/stripe/resource.py
@@ -117,6 +117,12 @@ class StripeObject(dict):
         except KeyError, err:
             raise AttributeError(*err.args)
 
+    def __delattr__(self, k):
+        if k[0] == '_' or k in self.__dict__:
+            return super(StripeObject, self).__delattr__(k)
+        else:
+            del self[k]
+
     def __setitem__(self, k, v):
         if v == "":
             raise ValueError(
@@ -149,9 +155,11 @@ class StripeObject(dict):
                 raise err
 
     def __delitem__(self, k):
-        raise TypeError(
-            "You cannot delete attributes on a StripeObject. "
-            "To unset a property, set it to None.")
+        super(StripeObject, self).__delitem__(k)
+
+        # Allows for unpickling in Python 3.x
+        if hasattr(self, '_unsaved_values'):
+            self._unsaved_values.remove(k)
 
     @classmethod
     def construct_from(cls, values, key, stripe_account=None):
diff --git a/stripe/test/test_resources.py b/stripe/test/test_resources.py
index 036704b6..093703d4 100644
--- a/stripe/test/test_resources.py
+++ b/stripe/test/test_resources.py
@@ -53,7 +53,6 @@ class StripeObjectTests(StripeUnitTestCase):
 
         # Illegal operations
         self.assertRaises(ValueError, setattr, obj, 'foo', '')
-        self.assertRaises(TypeError, obj.__delitem__, 'myattr')
 
     def test_refresh_from(self):
         obj = stripe.resource.StripeObject.construct_from({
@@ -163,6 +162,18 @@ class StripeObjectTests(StripeUnitTestCase):
         self.assertEqual('boo', newobj['object'])
         self.assertEqual('lalala', newobj.fala)
 
+    def test_deletion(self):
+        obj = stripe.resource.StripeObject('id', 'key')
+
+        obj.coupon = "foo"
+        self.assertEqual('foo', obj.coupon)
+
+        del obj.coupon
+        self.assertRaises(AttributeError, getattr, obj, 'coupon')
+
+        obj.refresh_from({'coupon': 'foo'}, api_key='bar', partial=True)
+        self.assertEqual('foo', obj.coupon)
+
 
 class ListObjectTests(StripeApiTestCase):
 
@@ -1068,6 +1079,22 @@ class CustomerTest(StripeResourceTest):
             {'Idempotency-Key': 'foo'}
         )
 
+    def test_del_coupon(self):
+        customer = stripe.Customer(id="cus_unset_desc")
+        customer.description = "bar"
+        customer.coupon = "foo"
+        del customer.coupon
+        customer.save()
+
+        self.requestor_mock.request.assert_called_with(
+            'post',
+            '/v1/customers/cus_unset_desc',
+            {
+                'description': 'bar'
+            },
+            None
+        )
+
     def test_cannot_set_empty_string(self):
         customer = stripe.Customer()
         self.assertRaises(ValueError, setattr, customer, "description", "")