From 296273ec229acb24fbbba1517601ba1b27e4701e Mon Sep 17 00:00:00 2001 From: David Brownman Date: Fri, 20 Mar 2026 16:15:47 -0700 Subject: [PATCH] return None for .deleted --- stripe/_account.py | 1 + stripe/_apple_pay_domain.py | 1 + stripe/_application.py | 1 + stripe/_bank_account.py | 1 + stripe/_card.py | 1 + stripe/_coupon.py | 1 + stripe/_customer.py | 1 + stripe/_discount.py | 1 + stripe/_invoice.py | 1 + stripe/_invoice_item.py | 1 + stripe/_person.py | 1 + stripe/_plan.py | 1 + stripe/_price.py | 1 + stripe/_product.py | 1 + stripe/_product_feature.py | 1 + stripe/_stripe_object.py | 13 +++++++++---- stripe/_subscription_item.py | 1 + stripe/_tax_id.py | 1 + stripe/_webhook_endpoint.py | 1 + stripe/apps/_secret.py | 1 + stripe/radar/_value_list.py | 1 + stripe/radar/_value_list_item.py | 1 + stripe/terminal/_configuration.py | 1 + stripe/terminal/_location.py | 1 + stripe/terminal/_reader.py | 1 + stripe/test_helpers/_test_clock.py | 1 + tests/test_stripe_object.py | 29 +++++++++++++++++++++++++++++ 27 files changed, 63 insertions(+), 4 deletions(-) diff --git a/stripe/_account.py b/stripe/_account.py index dc8c88026..eb1e97440 100644 --- a/stripe/_account.py +++ b/stripe/_account.py @@ -98,6 +98,7 @@ class Account( """ OBJECT_NAME: ClassVar[Literal["account"]] = "account" + _has_deleted_version = True class BusinessProfile(StripeObject): class AnnualRevenue(StripeObject): diff --git a/stripe/_apple_pay_domain.py b/stripe/_apple_pay_domain.py index c1691cf24..ee59523ec 100644 --- a/stripe/_apple_pay_domain.py +++ b/stripe/_apple_pay_domain.py @@ -29,6 +29,7 @@ class ApplePayDomain( ListableAPIResource["ApplePayDomain"], ): OBJECT_NAME: ClassVar[Literal["apple_pay_domain"]] = "apple_pay_domain" + _has_deleted_version = True created: int """ Time at which the object was created. Measured in seconds since the Unix epoch. diff --git a/stripe/_application.py b/stripe/_application.py index dee1e54fd..ab8043943 100644 --- a/stripe/_application.py +++ b/stripe/_application.py @@ -7,6 +7,7 @@ class Application(StripeObject): OBJECT_NAME: ClassVar[Literal["application"]] = "application" + _has_deleted_version = True deleted: Optional[Literal[True]] """ Always true for a deleted object diff --git a/stripe/_bank_account.py b/stripe/_bank_account.py index 49597e4ba..a4f422726 100644 --- a/stripe/_bank_account.py +++ b/stripe/_bank_account.py @@ -35,6 +35,7 @@ class BankAccount( """ OBJECT_NAME: ClassVar[Literal["bank_account"]] = "bank_account" + _has_deleted_version = True class FutureRequirements(StripeObject): class Error(StripeObject): diff --git a/stripe/_card.py b/stripe/_card.py index ad341595d..b4b54bcfa 100644 --- a/stripe/_card.py +++ b/stripe/_card.py @@ -26,6 +26,7 @@ class Card(DeletableAPIResource["Card"], UpdateableAPIResource["Card"]): """ OBJECT_NAME: ClassVar[Literal["card"]] = "card" + _has_deleted_version = True class Networks(StripeObject): preferred: Optional[str] diff --git a/stripe/_coupon.py b/stripe/_coupon.py index cf0b49440..96b0ca6ad 100644 --- a/stripe/_coupon.py +++ b/stripe/_coupon.py @@ -31,6 +31,7 @@ class Coupon( """ OBJECT_NAME: ClassVar[Literal["coupon"]] = "coupon" + _has_deleted_version = True class AppliesTo(StripeObject): products: List[str] diff --git a/stripe/_customer.py b/stripe/_customer.py index 01f7bcf0b..4e33dde52 100644 --- a/stripe/_customer.py +++ b/stripe/_customer.py @@ -132,6 +132,7 @@ class Customer( """ OBJECT_NAME: ClassVar[Literal["customer"]] = "customer" + _has_deleted_version = True class Address(StripeObject): city: Optional[str] diff --git a/stripe/_discount.py b/stripe/_discount.py index 1637edc79..a9689d764 100644 --- a/stripe/_discount.py +++ b/stripe/_discount.py @@ -20,6 +20,7 @@ class Discount(StripeObject): """ OBJECT_NAME: ClassVar[Literal["discount"]] = "discount" + _has_deleted_version = True class Source(StripeObject): coupon: Optional[ExpandableField["Coupon"]] diff --git a/stripe/_invoice.py b/stripe/_invoice.py index 518ccd594..5a837c0c0 100644 --- a/stripe/_invoice.py +++ b/stripe/_invoice.py @@ -124,6 +124,7 @@ class Invoice( """ OBJECT_NAME: ClassVar[Literal["invoice"]] = "invoice" + _has_deleted_version = True class AutomaticTax(StripeObject): class Liability(StripeObject): diff --git a/stripe/_invoice_item.py b/stripe/_invoice_item.py index b507ee8ae..4652ad73b 100644 --- a/stripe/_invoice_item.py +++ b/stripe/_invoice_item.py @@ -51,6 +51,7 @@ class InvoiceItem( """ OBJECT_NAME: ClassVar[Literal["invoiceitem"]] = "invoiceitem" + _has_deleted_version = True class Parent(StripeObject): class SubscriptionDetails(StripeObject): diff --git a/stripe/_person.py b/stripe/_person.py index 850f382a6..321c32610 100644 --- a/stripe/_person.py +++ b/stripe/_person.py @@ -22,6 +22,7 @@ class Person(UpdateableAPIResource["Person"]): """ OBJECT_NAME: ClassVar[Literal["person"]] = "person" + _has_deleted_version = True class AdditionalTosAcceptances(StripeObject): class Account(StripeObject): diff --git a/stripe/_plan.py b/stripe/_plan.py index fbf1c59e2..b3ba291c5 100644 --- a/stripe/_plan.py +++ b/stripe/_plan.py @@ -38,6 +38,7 @@ class Plan( """ OBJECT_NAME: ClassVar[Literal["plan"]] = "plan" + _has_deleted_version = True class Tier(StripeObject): flat_amount: Optional[int] diff --git a/stripe/_price.py b/stripe/_price.py index 79e83621f..bd460597d 100644 --- a/stripe/_price.py +++ b/stripe/_price.py @@ -45,6 +45,7 @@ class Price( """ OBJECT_NAME: ClassVar[Literal["price"]] = "price" + _has_deleted_version = True class CurrencyOptions(StripeObject): class CustomUnitAmount(StripeObject): diff --git a/stripe/_product.py b/stripe/_product.py index 9ebec26fe..5c1cac654 100644 --- a/stripe/_product.py +++ b/stripe/_product.py @@ -67,6 +67,7 @@ class Product( """ OBJECT_NAME: ClassVar[Literal["product"]] = "product" + _has_deleted_version = True class MarketingFeature(StripeObject): name: Optional[str] diff --git a/stripe/_product_feature.py b/stripe/_product_feature.py index 580ede870..745408f70 100644 --- a/stripe/_product_feature.py +++ b/stripe/_product_feature.py @@ -15,6 +15,7 @@ class ProductFeature(StripeObject): """ OBJECT_NAME: ClassVar[Literal["product_feature"]] = "product_feature" + _has_deleted_version = True deleted: Optional[Literal[True]] """ Always true for a deleted object diff --git a/stripe/_stripe_object.py b/stripe/_stripe_object.py index 5109cdb29..8225814b1 100644 --- a/stripe/_stripe_object.py +++ b/stripe/_stripe_object.py @@ -90,6 +90,8 @@ def default(self, o: Any) -> Any: _retrieve_params: Mapping[str, Any] _previous: Optional[Mapping[str, Any]] + # overridden on a per-resource basis in codegen + _has_deleted_version = False def __init__( self, @@ -143,10 +145,8 @@ def last_response(self) -> Optional[StripeResponse]: return self._last_response def update(self, update_dict: Mapping[str, Any]) -> None: - for k in update_dict: - self._unsaved_values.add(k) - - self._data.update(update_dict) + for k, v in update_dict.items(): + self[k] = v if not TYPE_CHECKING: @@ -213,6 +213,11 @@ def __getitem__(self, k: str) -> Any: "available on this object are: %s" % (k, k, ", ".join(list(self._data.keys()))) ) + elif k == "deleted" and self._has_deleted_version: + # certain objects have a `deleted` property that's None or true. + # Because of the way missing property access works, you couldn't write `if customer.deleted` because that was either true or an error. + # so to support this check specifically, we return the default value rather than erroring + return None else: from stripe._invoice import Invoice diff --git a/stripe/_subscription_item.py b/stripe/_subscription_item.py index 6db7dbfb1..bd0b5baf1 100644 --- a/stripe/_subscription_item.py +++ b/stripe/_subscription_item.py @@ -45,6 +45,7 @@ class SubscriptionItem( """ OBJECT_NAME: ClassVar[Literal["subscription_item"]] = "subscription_item" + _has_deleted_version = True class BillingThresholds(StripeObject): usage_gte: Optional[int] diff --git a/stripe/_tax_id.py b/stripe/_tax_id.py index c50ced8cc..7b2df69e1 100644 --- a/stripe/_tax_id.py +++ b/stripe/_tax_id.py @@ -33,6 +33,7 @@ class TaxId( """ OBJECT_NAME: ClassVar[Literal["tax_id"]] = "tax_id" + _has_deleted_version = True class Owner(StripeObject): account: Optional[ExpandableField["Account"]] diff --git a/stripe/_webhook_endpoint.py b/stripe/_webhook_endpoint.py index 4ea07b9f8..9a5720e4a 100644 --- a/stripe/_webhook_endpoint.py +++ b/stripe/_webhook_endpoint.py @@ -44,6 +44,7 @@ class WebhookEndpoint( """ OBJECT_NAME: ClassVar[Literal["webhook_endpoint"]] = "webhook_endpoint" + _has_deleted_version = True api_version: Optional[str] """ The API version events are rendered as for this webhook endpoint. diff --git a/stripe/apps/_secret.py b/stripe/apps/_secret.py index 6a6a9e756..17755d139 100644 --- a/stripe/apps/_secret.py +++ b/stripe/apps/_secret.py @@ -30,6 +30,7 @@ class Secret(CreateableAPIResource["Secret"], ListableAPIResource["Secret"]): """ OBJECT_NAME: ClassVar[Literal["apps.secret"]] = "apps.secret" + _has_deleted_version = True class Scope(StripeObject): type: Literal["account", "user"] diff --git a/stripe/radar/_value_list.py b/stripe/radar/_value_list.py index bc4521b6a..2bc484ee0 100644 --- a/stripe/radar/_value_list.py +++ b/stripe/radar/_value_list.py @@ -39,6 +39,7 @@ class ValueList( """ OBJECT_NAME: ClassVar[Literal["radar.value_list"]] = "radar.value_list" + _has_deleted_version = True alias: str """ The name of the value list for use in rules. diff --git a/stripe/radar/_value_list_item.py b/stripe/radar/_value_list_item.py index b9010a26c..6e8ad898b 100644 --- a/stripe/radar/_value_list_item.py +++ b/stripe/radar/_value_list_item.py @@ -37,6 +37,7 @@ class ValueListItem( OBJECT_NAME: ClassVar[Literal["radar.value_list_item"]] = ( "radar.value_list_item" ) + _has_deleted_version = True created: int """ Time at which the object was created. Measured in seconds since the Unix epoch. diff --git a/stripe/terminal/_configuration.py b/stripe/terminal/_configuration.py index 09065bd32..d6193869c 100644 --- a/stripe/terminal/_configuration.py +++ b/stripe/terminal/_configuration.py @@ -44,6 +44,7 @@ class Configuration( OBJECT_NAME: ClassVar[Literal["terminal.configuration"]] = ( "terminal.configuration" ) + _has_deleted_version = True class BbposWisepad3(StripeObject): splashscreen: Optional[ExpandableField["File"]] diff --git a/stripe/terminal/_location.py b/stripe/terminal/_location.py index 77fec24cf..ee3816c86 100644 --- a/stripe/terminal/_location.py +++ b/stripe/terminal/_location.py @@ -39,6 +39,7 @@ class Location( """ OBJECT_NAME: ClassVar[Literal["terminal.location"]] = "terminal.location" + _has_deleted_version = True class Address(StripeObject): city: Optional[str] diff --git a/stripe/terminal/_reader.py b/stripe/terminal/_reader.py index cbba63d09..e5329f719 100644 --- a/stripe/terminal/_reader.py +++ b/stripe/terminal/_reader.py @@ -74,6 +74,7 @@ class Reader( """ OBJECT_NAME: ClassVar[Literal["terminal.reader"]] = "terminal.reader" + _has_deleted_version = True class Action(StripeObject): class CollectInputs(StripeObject): diff --git a/stripe/test_helpers/_test_clock.py b/stripe/test_helpers/_test_clock.py index 7e011186b..f8b314d58 100644 --- a/stripe/test_helpers/_test_clock.py +++ b/stripe/test_helpers/_test_clock.py @@ -41,6 +41,7 @@ class TestClock( OBJECT_NAME: ClassVar[Literal["test_helpers.test_clock"]] = ( "test_helpers.test_clock" ) + _has_deleted_version = True class StatusDetails(StripeObject): class Advancing(StripeObject): diff --git a/tests/test_stripe_object.py b/tests/test_stripe_object.py index 430fe81cd..89d3ddbbb 100644 --- a/tests/test_stripe_object.py +++ b/tests/test_stripe_object.py @@ -6,8 +6,10 @@ import pytest import stripe +from stripe._customer import Customer from stripe._invoice import Invoice from stripe._stripe_object import StripeObject +from stripe.billing._alert import Alert # We use this because it has a map, "restriction.currency_options" from string -> CurrencyOptions nested class. SAMPLE_PROMOTION_CODE = json.loads( @@ -313,6 +315,33 @@ def test_deletion_metadata(self): with pytest.raises(KeyError): obj.metadata["key"] + def test_non_deleted_deletable_resource(self): + """ + if it's possible for an object to be deleted, you should be able to read th + """ + customer = Customer() + assert customer.deleted is None + assert hasattr(customer, "deleted") + # this is a little confusing. since we're faking that this property exist, checks that access the underlying data disagree with property access + assert "deleted" not in customer + + def test_deleted_property_reads(self): + """ + certain objects have a `deleted` property that's None or true. Because of the way missing property access works, you couldn't write `if customer.deleted` because that was either true or an error. + + Instead, we now initialize that property on objects that can be `deleted`, so the check works as expected + """ + customer = Customer.construct_from({"deleted": True}, None) + assert customer.deleted + assert hasattr(customer, "deleted") + assert "deleted" in customer + + def test_no_deleted_property_fails(self): + # an alert can never be `deleted`, so this should always error + alert = Alert() + with pytest.raises(AttributeError): + alert.deleted + def test_copy(self): nested = StripeObject.construct_from({"value": "bar"}, "mykey") obj = StripeObject.construct_from(