|
5 | 5 | from decimal import Decimal |
6 | 6 | from enum import Enum |
7 | 7 | from urllib.parse import quote |
| 8 | +from weakref import WeakKeyDictionary |
8 | 9 |
|
9 | 10 | import pytest |
10 | 11 |
|
11 | 12 | from qs_codec import Charset, EncodeOptions, Format, ListFormat, dumps, encode |
| 13 | +from qs_codec.encode import _encode, _sentinel |
12 | 14 | from qs_codec.models.undefined import Undefined |
| 15 | +from qs_codec.models.weak_wrapper import WeakWrapper |
13 | 16 | from qs_codec.utils.encode_utils import EncodeUtils |
| 17 | +from qs_codec.utils.utils import Utils |
14 | 18 |
|
15 | 19 |
|
16 | 20 | class TestEncode: |
@@ -1691,3 +1695,113 @@ def test_encodes_non_string_keys(self) -> None: |
1691 | 1695 | ) |
1692 | 1696 | == "a=b" |
1693 | 1697 | ) |
| 1698 | + |
| 1699 | + |
| 1700 | +class TestEncodeInternals: |
| 1701 | + def test_encode_cycle_detection_raises_on_same_step(self) -> None: |
| 1702 | + value: t.Dict[str, t.Any] = {"loop": {}} |
| 1703 | + side_channel: WeakKeyDictionary = WeakKeyDictionary() |
| 1704 | + parent_channel: WeakKeyDictionary = WeakKeyDictionary() |
| 1705 | + wrapper = WeakWrapper(value) |
| 1706 | + parent_channel[wrapper] = 1 |
| 1707 | + side_channel[_sentinel] = parent_channel |
| 1708 | + |
| 1709 | + with pytest.raises(ValueError, match="Circular reference detected"): |
| 1710 | + _encode( |
| 1711 | + value=value, |
| 1712 | + is_undefined=False, |
| 1713 | + side_channel=side_channel, |
| 1714 | + prefix="root", |
| 1715 | + comma_round_trip=False, |
| 1716 | + encoder=EncodeUtils.encode, |
| 1717 | + serialize_date=EncodeUtils.serialize_date, |
| 1718 | + sort=None, |
| 1719 | + filter=None, |
| 1720 | + formatter=Format.RFC3986.formatter, |
| 1721 | + format=Format.RFC3986, |
| 1722 | + generate_array_prefix=ListFormat.INDICES.generator, |
| 1723 | + allow_empty_lists=False, |
| 1724 | + strict_null_handling=False, |
| 1725 | + skip_nulls=False, |
| 1726 | + encode_dot_in_keys=False, |
| 1727 | + allow_dots=False, |
| 1728 | + encode_values_only=False, |
| 1729 | + charset=Charset.UTF8, |
| 1730 | + ) |
| 1731 | + |
| 1732 | + def test_encode_cycle_detection_marks_prior_visit_without_raising(self) -> None: |
| 1733 | + value: t.Dict[str, t.Any] = {"child": "value"} |
| 1734 | + side_channel: WeakKeyDictionary = WeakKeyDictionary() |
| 1735 | + parent_channel: WeakKeyDictionary = WeakKeyDictionary() |
| 1736 | + wrapper = WeakWrapper(value) |
| 1737 | + parent_channel[wrapper] = 99 |
| 1738 | + side_channel[_sentinel] = parent_channel |
| 1739 | + |
| 1740 | + tokens = _encode( |
| 1741 | + value=value, |
| 1742 | + is_undefined=False, |
| 1743 | + side_channel=side_channel, |
| 1744 | + prefix="root", |
| 1745 | + comma_round_trip=False, |
| 1746 | + encoder=EncodeUtils.encode, |
| 1747 | + serialize_date=EncodeUtils.serialize_date, |
| 1748 | + sort=None, |
| 1749 | + filter=None, |
| 1750 | + formatter=Format.RFC3986.formatter, |
| 1751 | + format=Format.RFC3986, |
| 1752 | + generate_array_prefix=ListFormat.INDICES.generator, |
| 1753 | + allow_empty_lists=False, |
| 1754 | + strict_null_handling=False, |
| 1755 | + skip_nulls=False, |
| 1756 | + encode_dot_in_keys=False, |
| 1757 | + allow_dots=False, |
| 1758 | + encode_values_only=False, |
| 1759 | + charset=Charset.UTF8, |
| 1760 | + ) |
| 1761 | + |
| 1762 | + assert tokens == ["root%5Bchild%5D=value"] |
| 1763 | + |
| 1764 | + def test_encode_handles_iterable_filter_for_indexable_object(self, monkeypatch: pytest.MonkeyPatch) -> None: |
| 1765 | + class Indexable: |
| 1766 | + def __getitem__(self, key: str) -> str: |
| 1767 | + if key != "foo": |
| 1768 | + raise KeyError(key) |
| 1769 | + return "bar" |
| 1770 | + |
| 1771 | + original = Utils.is_non_nullish_primitive |
| 1772 | + |
| 1773 | + def fake_is_non_nullish_primitive(val: t.Any, skip_nulls: bool = False) -> bool: |
| 1774 | + if isinstance(val, Indexable): |
| 1775 | + return False |
| 1776 | + return original(val, skip_nulls) |
| 1777 | + |
| 1778 | + monkeypatch.setattr(Utils, "is_non_nullish_primitive", staticmethod(fake_is_non_nullish_primitive)) |
| 1779 | + |
| 1780 | + tokens = _encode( |
| 1781 | + value=Indexable(), |
| 1782 | + is_undefined=False, |
| 1783 | + side_channel=WeakKeyDictionary(), |
| 1784 | + prefix="root", |
| 1785 | + comma_round_trip=False, |
| 1786 | + encoder=EncodeUtils.encode, |
| 1787 | + serialize_date=EncodeUtils.serialize_date, |
| 1788 | + sort=None, |
| 1789 | + filter=["foo"], |
| 1790 | + formatter=Format.RFC3986.formatter, |
| 1791 | + format=Format.RFC3986, |
| 1792 | + generate_array_prefix=ListFormat.INDICES.generator, |
| 1793 | + allow_empty_lists=False, |
| 1794 | + strict_null_handling=False, |
| 1795 | + skip_nulls=False, |
| 1796 | + encode_dot_in_keys=False, |
| 1797 | + allow_dots=False, |
| 1798 | + encode_values_only=False, |
| 1799 | + charset=Charset.UTF8, |
| 1800 | + ) |
| 1801 | + |
| 1802 | + assert tokens == ["root%5Bfoo%5D=bar"] |
| 1803 | + |
| 1804 | + def test_encode_comma_format_serializes_datetime_without_custom_callable(self) -> None: |
| 1805 | + dt = datetime(2024, 1, 1) |
| 1806 | + options = EncodeOptions(list_format=ListFormat.COMMA, serialize_date="iso") |
| 1807 | + assert encode({"a": [dt]}, options) == f"a={dt.isoformat().replace(':', '%3A')}" |
0 commit comments