From f72f1e5dac9b8a5ec8ac1b3cab22c0c4501e0f15 Mon Sep 17 00:00:00 2001 From: Praniket Walavalkar Date: Tue, 25 Nov 2025 10:38:11 -0800 Subject: [PATCH 1/4] Fix DeprecationWarning when encoding StripeObject metadata (fixes #1651) --- stripe/_encode.py | 9 ++++- tests/test_encode.py | 96 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 tests/test_encode.py diff --git a/stripe/_encode.py b/stripe/_encode.py index 228edfdc6..530429adf 100644 --- a/stripe/_encode.py +++ b/stripe/_encode.py @@ -31,8 +31,13 @@ def _api_encode(data) -> Generator[Tuple[str, Any], None, None]: for key, value in data.items(): if value is None: continue - elif hasattr(value, "stripe_id"): - yield (key, value.stripe_id) + elif ( + hasattr(value, "get") + and callable(value.get) + and value.get("id") is not None + ): + # Check if value is a StripeObject with an 'id' without triggering deprecated stripe_id property + yield (key, value.get("id")) elif isinstance(value, list) or isinstance(value, tuple): for i, sv in enumerate(value): # Always use indexed format for arrays diff --git a/tests/test_encode.py b/tests/test_encode.py new file mode 100644 index 000000000..6aabc5c55 --- /dev/null +++ b/tests/test_encode.py @@ -0,0 +1,96 @@ +import warnings + +from stripe._stripe_object import StripeObject +from stripe._encode import _api_encode + + +class TestApiEncode: + def test_encode_stripe_object_without_id_no_deprecation_warning(self): + """ + Test that encoding a StripeObject without an id (like metadata) + does not trigger a deprecation warning. + Regression test for issue #1651. + """ + metadata = StripeObject() + metadata["key1"] = "value1" + metadata["key2"] = "value2" + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = list(_api_encode({"metadata": metadata})) + + # Check no deprecation warnings were raised + deprecation_warnings = [ + warning + for warning in w + if issubclass(warning.category, DeprecationWarning) + ] + assert len(deprecation_warnings) == 0, ( + f"Expected no deprecation warnings, but got {len(deprecation_warnings)}" + ) + + # Verify the metadata was encoded correctly as nested dict + assert result == [ + ("metadata[key1]", "value1"), + ("metadata[key2]", "value2"), + ] + + def test_encode_stripe_object_with_id_extracts_id(self): + """ + Test that encoding a StripeObject with an id (like a customer reference) + correctly extracts just the id value. + """ + customer = StripeObject() + customer["id"] = "cus_123" + customer["name"] = "Test Customer" + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = list(_api_encode({"customer": customer})) + + # Check no deprecation warnings were raised + deprecation_warnings = [ + warning + for warning in w + if issubclass(warning.category, DeprecationWarning) + ] + assert len(deprecation_warnings) == 0, ( + f"Expected no deprecation warnings, but got {len(deprecation_warnings)}" + ) + + # Should encode to just the ID + assert result == [("customer", "cus_123")] + + def test_encode_regular_dict(self): + """Test that regular dicts are encoded as nested dicts.""" + regular_dict = {"key1": "value1", "key2": "value2"} + result = list(_api_encode({"data": regular_dict})) + + assert result == [ + ("data[key1]", "value1"), + ("data[key2]", "value2"), + ] + + def test_encode_none_value_skipped(self): + """Test that None values are skipped during encoding.""" + result = list(_api_encode({"field": None})) + assert result == [] + + def test_encode_string_value(self): + """Test that string values are encoded directly.""" + result = list(_api_encode({"name": "John Doe"})) + assert result == [("name", "John Doe")] + + def test_encode_boolean_value(self): + """Test that boolean values are encoded as lowercase strings.""" + result = list(_api_encode({"active": True, "deleted": False})) + assert result == [("active", "true"), ("deleted", "false")] + + def test_encode_list_value(self): + """Test that list values are encoded with indexed keys.""" + result = list(_api_encode({"items": ["item1", "item2", "item3"]})) + assert result == [ + ("items[0]", "item1"), + ("items[1]", "item2"), + ("items[2]", "item3"), + ] From 767807a2aeec1a65e61169f2fe0b4bde0e6c100e Mon Sep 17 00:00:00 2001 From: Praniket Walavalkar Date: Sat, 13 Dec 2025 21:27:26 -0800 Subject: [PATCH 2/4] Suppress deprecation warnings during internal encoding to fix metadata updates --- stripe/_encode.py | 58 ++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/stripe/_encode.py b/stripe/_encode.py index 530429adf..d6e535789 100644 --- a/stripe/_encode.py +++ b/stripe/_encode.py @@ -1,6 +1,7 @@ import calendar import datetime import time +import warnings from collections import OrderedDict from typing import Generator, Tuple, Any @@ -31,30 +32,35 @@ def _api_encode(data) -> Generator[Tuple[str, Any], None, None]: for key, value in data.items(): if value is None: continue - elif ( - hasattr(value, "get") - and callable(value.get) - and value.get("id") is not None - ): - # Check if value is a StripeObject with an 'id' without triggering deprecated stripe_id property - yield (key, value.get("id")) - elif isinstance(value, list) or isinstance(value, tuple): - for i, sv in enumerate(value): - # Always use indexed format for arrays - encoded_key = "%s[%d]" % (key, i) - if isinstance(sv, dict): - subdict = _encode_nested_dict(encoded_key, sv) - for k, v in _api_encode(subdict): - yield (k, v) - else: - yield (encoded_key, sv) - elif isinstance(value, dict): - subdict = _encode_nested_dict(key, value) - for subkey, subvalue in _api_encode(subdict): - yield (subkey, subvalue) - elif isinstance(value, datetime.datetime): - yield (key, _encode_datetime(value)) - elif isinstance(value, bool): - yield (key, str(value).lower()) else: - yield (key, value) + # Check if value has stripe_id attribute, suppressing deprecation warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + has_stripe_id = hasattr(value, "stripe_id") + + if has_stripe_id: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + yield (key, value.stripe_id) + continue + + if isinstance(value, list) or isinstance(value, tuple): + for i, sv in enumerate(value): + # Always use indexed format for arrays + encoded_key = "%s[%d]" % (key, i) + if isinstance(sv, dict): + subdict = _encode_nested_dict(encoded_key, sv) + for k, v in _api_encode(subdict): + yield (k, v) + else: + yield (encoded_key, sv) + elif isinstance(value, dict): + subdict = _encode_nested_dict(key, value) + for subkey, subvalue in _api_encode(subdict): + yield (subkey, subvalue) + elif isinstance(value, datetime.datetime): + yield (key, _encode_datetime(value)) + elif isinstance(value, bool): + yield (key, str(value).lower()) + else: + yield (key, value) From 84823f348506584ca1e2ff22d2ae8b04574453c6 Mon Sep 17 00:00:00 2001 From: Praniket Walavalkar Date: Sat, 20 Dec 2025 13:20:43 -0800 Subject: [PATCH 3/4] Use getattr for id access to avoid stripe_id deprecation warning --- stripe/_encode.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/stripe/_encode.py b/stripe/_encode.py index d6e535789..812036aad 100644 --- a/stripe/_encode.py +++ b/stripe/_encode.py @@ -33,18 +33,14 @@ def _api_encode(data) -> Generator[Tuple[str, Any], None, None]: if value is None: continue else: - # Check if value has stripe_id attribute, suppressing deprecation warnings + # Check for stripe_id attribute without triggering deprecation warning with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) has_stripe_id = hasattr(value, "stripe_id") - if has_stripe_id: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - yield (key, value.stripe_id) - continue - - if isinstance(value, list) or isinstance(value, tuple): + if has_stripe_id and hasattr(value, "id"): + yield (key, getattr(value, "id")) + elif isinstance(value, list) or isinstance(value, tuple): for i, sv in enumerate(value): # Always use indexed format for arrays encoded_key = "%s[%d]" % (key, i) From 9feb0b3a479664fbe1f4d27358b327bf199d6983 Mon Sep 17 00:00:00 2001 From: Praniket Walavalkar Date: Sat, 20 Dec 2025 13:23:30 -0800 Subject: [PATCH 4/4] Handle None id values correctly in encoding --- stripe/_encode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stripe/_encode.py b/stripe/_encode.py index 812036aad..018417e02 100644 --- a/stripe/_encode.py +++ b/stripe/_encode.py @@ -38,7 +38,7 @@ def _api_encode(data) -> Generator[Tuple[str, Any], None, None]: warnings.simplefilter("ignore", DeprecationWarning) has_stripe_id = hasattr(value, "stripe_id") - if has_stripe_id and hasattr(value, "id"): + if has_stripe_id and hasattr(value, "id") and getattr(value, "id") is not None: yield (key, getattr(value, "id")) elif isinstance(value, list) or isinstance(value, tuple): for i, sv in enumerate(value):