diff --git a/stripe/_encode.py b/stripe/_encode.py index 228edfdc6..018417e02 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,25 +32,31 @@ 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 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 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 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): + # 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) 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"), + ]