qs-codec 1.5.2__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {qs_codec-1.5.2 → qs_codec-1.6.0}/CHANGELOG.md +4 -0
  2. {qs_codec-1.5.2 → qs_codec-1.6.0}/PKG-INFO +62 -9
  3. {qs_codec-1.5.2 → qs_codec-1.6.0}/README.rst +61 -8
  4. {qs_codec-1.5.2 → qs_codec-1.6.0}/docs/README.rst +61 -8
  5. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/__init__.py +1 -3
  6. qs_codec-1.6.0/src/qs_codec/enums/sentinel.py +50 -0
  7. qs_codec-1.6.0/src/qs_codec/models/undefined.py +62 -0
  8. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/package.json +1 -1
  9. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/example_test.py +0 -3
  10. qs_codec-1.6.0/tests/unit/package_test.py +18 -0
  11. qs_codec-1.5.2/src/qs_codec/enums/sentinel.py +0 -50
  12. qs_codec-1.5.2/src/qs_codec/models/undefined.py +0 -73
  13. {qs_codec-1.5.2 → qs_codec-1.6.0}/.gitignore +0 -0
  14. {qs_codec-1.5.2 → qs_codec-1.6.0}/CODE-OF-CONDUCT.md +0 -0
  15. {qs_codec-1.5.2 → qs_codec-1.6.0}/LICENSE +0 -0
  16. {qs_codec-1.5.2 → qs_codec-1.6.0}/pyproject.toml +0 -0
  17. {qs_codec-1.5.2 → qs_codec-1.6.0}/requirements_dev.txt +0 -0
  18. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/constants/__init__.py +0 -0
  19. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/constants/encode_constants.py +0 -0
  20. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/decode.py +0 -0
  21. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/encode.py +0 -0
  22. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/__init__.py +0 -0
  23. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/charset.py +0 -0
  24. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/decode_kind.py +0 -0
  25. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/duplicates.py +0 -0
  26. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/format.py +0 -0
  27. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/list_format.py +0 -0
  28. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/__init__.py +0 -0
  29. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/cycle_state.py +0 -0
  30. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/decode_options.py +0 -0
  31. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/encode_frame.py +0 -0
  32. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/encode_options.py +0 -0
  33. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/key_path_node.py +0 -0
  34. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/overflow_dict.py +0 -0
  35. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/structured_key_scan.py +0 -0
  36. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/weak_wrapper.py +0 -0
  37. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/py.typed +0 -0
  38. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/utils/__init__.py +0 -0
  39. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/utils/decode_utils.py +0 -0
  40. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/utils/encode_utils.py +0 -0
  41. {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/utils/utils.py +0 -0
  42. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/.gitignore +0 -0
  43. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/__init__.py +0 -0
  44. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/compare_outputs.sh +0 -0
  45. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/pnpm-lock.yaml +0 -0
  46. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/pnpm-workspace.yaml +0 -0
  47. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/qs.js +0 -0
  48. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/qs.py +0 -0
  49. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/test_cases.json +0 -0
  50. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/e2e/__init__.py +0 -0
  51. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/e2e/e2e_test.py +0 -0
  52. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/__init__.py +0 -0
  53. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/decode_options_test.py +0 -0
  54. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/decode_test.py +0 -0
  55. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/encode_internal_helpers_test.py +0 -0
  56. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/encode_options_test.py +0 -0
  57. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/encode_test.py +0 -0
  58. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/fixed_qs_issues_test.py +0 -0
  59. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/key_path_node_test.py +0 -0
  60. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/list_format_test.py +0 -0
  61. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/thread_safety_test.py +0 -0
  62. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/utils_test.py +0 -0
  63. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/weakref_test.py +0 -0
  64. {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/wpt_urlencoded_parser_test.py +0 -0
@@ -1,3 +1,7 @@
1
+ ## 1.6.0
2
+
3
+ * [CHORE] make `Undefined` internal by removing `qs_codec.Undefined` and its public documentation
4
+
1
5
  ## 1.5.2
2
6
 
3
7
  * [CHORE] simplify mapping and key-iteration checks in encode/decode internals
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qs-codec
3
- Version: 1.5.2
3
+ Version: 1.6.0
4
4
  Summary: A query string encoding and decoding library for Python. Ported from qs for JavaScript.
5
5
  Project-URL: Homepage, https://techouse.github.io/qs_codec/
6
6
  Project-URL: Documentation, https://techouse.github.io/qs_codec/
@@ -112,6 +112,67 @@ A simple usage example:
112
112
  # Decoding
113
113
  assert qs.decode('a=b') == {'a': 'b'}
114
114
 
115
+ Working with URLs
116
+ ~~~~~~~~~~~~~~~~~
117
+
118
+ Use `urllib.parse.urlsplit <https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlsplit>`__
119
+ to keep URL parsing separate from query-string decoding. Pass the encoded
120
+ ``query`` component directly to ``qs.decode`` without calling ``unquote``,
121
+ ``unquote_plus``, ``parse_qs``, or ``parse_qsl`` first:
122
+
123
+ .. code:: python
124
+
125
+ from urllib.parse import urlsplit
126
+
127
+ import qs_codec as qs
128
+
129
+ parts = urlsplit(
130
+ 'https://example.com/search?filter%5Bname%5D=Jane%20Doe&flag#results'
131
+ )
132
+ params = qs.decode(
133
+ parts.query,
134
+ qs.DecodeOptions(strict_null_handling=True),
135
+ )
136
+
137
+ assert params == {
138
+ 'filter': {'name': 'Jane Doe'},
139
+ 'flag': None,
140
+ }
141
+
142
+ Passing the encoded component unchanged ensures that escaped delimiters such as
143
+ ``%26``, escaped percent signs such as ``%2525``, and encoded bracket syntax
144
+ reach ``qs.decode`` without being decoded twice.
145
+
146
+ To replace a URL query, encode fresh data and assign it to the split result's
147
+ ``query`` component:
148
+
149
+ .. code:: python
150
+
151
+ updated = parts._replace(
152
+ query=qs.encode({
153
+ 'filter': {'name': 'John Doe'},
154
+ 'tags': ['a', 'b'],
155
+ }),
156
+ ).geturl()
157
+
158
+ assert updated == (
159
+ 'https://example.com/search?'
160
+ 'filter%5Bname%5D=John%20Doe&tags%5B0%5D=a&tags%5B1%5D=b'
161
+ '#results'
162
+ )
163
+
164
+ Keep `EncodeOptions.add_query_prefix <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.add_query_prefix>`__
165
+ set to ``False`` (the default) when assigning to ``SplitResult.query``. Options
166
+ such as ``encode=False``, ``encode_values_only=True``, or a custom encoder can
167
+ emit raw URL-structural characters, so callers using them must ensure the result
168
+ is safe query-component text.
169
+
170
+ This pattern replaces the existing query; it does not append or merge it.
171
+ Appending or decoding and re-encoding an arbitrary query can change delimiter,
172
+ duplicate-key, name-only, list-format, ordering, and percent-encoding semantics.
173
+ ``SplitResult.geturl()`` may also normalize URL spelling and removes an explicit
174
+ empty ``?`` delimiter.
175
+
115
176
  Decoding
116
177
  ~~~~~~~~
117
178
 
@@ -752,14 +813,6 @@ Keys with no values (such as an empty ``dict`` or ``list``) will return nothing:
752
813
 
753
814
  assert qs.encode({'a': {'b': {}}}) == ''
754
815
 
755
- `Undefined <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.undefined.Undefined>`__ properties will be omitted entirely:
756
-
757
- .. code:: python
758
-
759
- import qs_codec as qs
760
-
761
- assert qs.encode({'a': None, 'b': qs.Undefined()}) == 'a='
762
-
763
816
  The query string may optionally be prepended with a question mark (``?``) by setting
764
817
  `add_query_prefix <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.add_query_prefix>`__ to ``True``:
765
818
 
@@ -49,6 +49,67 @@ A simple usage example:
49
49
  # Decoding
50
50
  assert qs.decode('a=b') == {'a': 'b'}
51
51
 
52
+ Working with URLs
53
+ ~~~~~~~~~~~~~~~~~
54
+
55
+ Use `urllib.parse.urlsplit <https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlsplit>`__
56
+ to keep URL parsing separate from query-string decoding. Pass the encoded
57
+ ``query`` component directly to ``qs.decode`` without calling ``unquote``,
58
+ ``unquote_plus``, ``parse_qs``, or ``parse_qsl`` first:
59
+
60
+ .. code:: python
61
+
62
+ from urllib.parse import urlsplit
63
+
64
+ import qs_codec as qs
65
+
66
+ parts = urlsplit(
67
+ 'https://example.com/search?filter%5Bname%5D=Jane%20Doe&flag#results'
68
+ )
69
+ params = qs.decode(
70
+ parts.query,
71
+ qs.DecodeOptions(strict_null_handling=True),
72
+ )
73
+
74
+ assert params == {
75
+ 'filter': {'name': 'Jane Doe'},
76
+ 'flag': None,
77
+ }
78
+
79
+ Passing the encoded component unchanged ensures that escaped delimiters such as
80
+ ``%26``, escaped percent signs such as ``%2525``, and encoded bracket syntax
81
+ reach ``qs.decode`` without being decoded twice.
82
+
83
+ To replace a URL query, encode fresh data and assign it to the split result's
84
+ ``query`` component:
85
+
86
+ .. code:: python
87
+
88
+ updated = parts._replace(
89
+ query=qs.encode({
90
+ 'filter': {'name': 'John Doe'},
91
+ 'tags': ['a', 'b'],
92
+ }),
93
+ ).geturl()
94
+
95
+ assert updated == (
96
+ 'https://example.com/search?'
97
+ 'filter%5Bname%5D=John%20Doe&tags%5B0%5D=a&tags%5B1%5D=b'
98
+ '#results'
99
+ )
100
+
101
+ Keep `EncodeOptions.add_query_prefix <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.add_query_prefix>`__
102
+ set to ``False`` (the default) when assigning to ``SplitResult.query``. Options
103
+ such as ``encode=False``, ``encode_values_only=True``, or a custom encoder can
104
+ emit raw URL-structural characters, so callers using them must ensure the result
105
+ is safe query-component text.
106
+
107
+ This pattern replaces the existing query; it does not append or merge it.
108
+ Appending or decoding and re-encoding an arbitrary query can change delimiter,
109
+ duplicate-key, name-only, list-format, ordering, and percent-encoding semantics.
110
+ ``SplitResult.geturl()`` may also normalize URL spelling and removes an explicit
111
+ empty ``?`` delimiter.
112
+
52
113
  Decoding
53
114
  ~~~~~~~~
54
115
 
@@ -689,14 +750,6 @@ Keys with no values (such as an empty ``dict`` or ``list``) will return nothing:
689
750
 
690
751
  assert qs.encode({'a': {'b': {}}}) == ''
691
752
 
692
- `Undefined <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.undefined.Undefined>`__ properties will be omitted entirely:
693
-
694
- .. code:: python
695
-
696
- import qs_codec as qs
697
-
698
- assert qs.encode({'a': None, 'b': qs.Undefined()}) == 'a='
699
-
700
753
  The query string may optionally be prepended with a question mark (``?``) by setting
701
754
  `add_query_prefix <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.add_query_prefix>`__ to ``True``:
702
755
 
@@ -10,6 +10,67 @@ Do not mutate caller-owned input containers or shared callback state while an
10
10
  free-threaded CPython build, which is supported and covered by the thread-safety
11
11
  test suite without changing these mutation guarantees.
12
12
 
13
+ Working with URLs
14
+ ~~~~~~~~~~~~~~~~~
15
+
16
+ Use `urllib.parse.urlsplit <https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlsplit>`_
17
+ to keep URL parsing separate from query-string decoding. Pass the encoded
18
+ ``query`` component directly to :py:attr:`qs_codec.decode` without calling
19
+ ``unquote``, ``unquote_plus``, ``parse_qs``, or ``parse_qsl`` first:
20
+
21
+ .. code:: python
22
+
23
+ from urllib.parse import urlsplit
24
+
25
+ import qs_codec as qs
26
+
27
+ parts = urlsplit(
28
+ 'https://example.com/search?filter%5Bname%5D=Jane%20Doe&flag#results'
29
+ )
30
+ params = qs.decode(
31
+ parts.query,
32
+ qs.DecodeOptions(strict_null_handling=True),
33
+ )
34
+
35
+ assert params == {
36
+ 'filter': {'name': 'Jane Doe'},
37
+ 'flag': None,
38
+ }
39
+
40
+ Passing the encoded component unchanged ensures that escaped delimiters such as
41
+ ``%26``, escaped percent signs such as ``%2525``, and encoded bracket syntax
42
+ reach :py:attr:`qs_codec.decode` without being decoded twice.
43
+
44
+ To replace a URL query, encode fresh data and assign it to the split result's
45
+ ``query`` component:
46
+
47
+ .. code:: python
48
+
49
+ updated = parts._replace(
50
+ query=qs.encode({
51
+ 'filter': {'name': 'John Doe'},
52
+ 'tags': ['a', 'b'],
53
+ }),
54
+ ).geturl()
55
+
56
+ assert updated == (
57
+ 'https://example.com/search?'
58
+ 'filter%5Bname%5D=John%20Doe&tags%5B0%5D=a&tags%5B1%5D=b'
59
+ '#results'
60
+ )
61
+
62
+ Keep :py:attr:`add_query_prefix <qs_codec.models.encode_options.EncodeOptions.add_query_prefix>`
63
+ set to ``False`` (the default) when assigning to ``SplitResult.query``. Options
64
+ such as ``encode=False``, ``encode_values_only=True``, or a custom encoder can
65
+ emit raw URL-structural characters, so callers using them must ensure the result
66
+ is safe query-component text.
67
+
68
+ This pattern replaces the existing query; it does not append or merge it.
69
+ Appending or decoding and re-encoding an arbitrary query can change delimiter,
70
+ duplicate-key, name-only, list-format, ordering, and percent-encoding semantics.
71
+ ``SplitResult.geturl()`` may also normalize URL spelling and removes an explicit
72
+ empty ``?`` delimiter.
73
+
13
74
  Decoding
14
75
  ~~~~~~~~
15
76
 
@@ -683,14 +744,6 @@ Keys with no values (such as an empty ``dict`` or ``list``) will return nothing:
683
744
 
684
745
  assert qs.encode({'a': {'b': {}}}) == ''
685
746
 
686
- :py:attr:`Undefined <qs_codec.models.undefined.Undefined>` properties will be omitted entirely:
687
-
688
- .. code:: python
689
-
690
- import qs_codec as qs
691
-
692
- assert qs.encode({'a': None, 'b': qs.Undefined()}) == 'a='
693
-
694
747
  The query string may optionally be prepended with a question mark (``?``) by setting
695
748
  :py:attr:`add_query_prefix <qs_codec.models.encode_options.EncodeOptions.add_query_prefix>` to ``True``:
696
749
 
@@ -14,7 +14,7 @@ The package root re-exports the most commonly used functions and enums so you ca
14
14
  """
15
15
 
16
16
  # Package version (PEP 440). Bump in lockstep with distribution metadata.
17
- __version__ = "1.5.2"
17
+ __version__ = "1.6.0"
18
18
 
19
19
  # Public API surface re-exported at the package root.
20
20
  __all__ = [
@@ -31,7 +31,6 @@ __all__ = [
31
31
  "Sentinel",
32
32
  "DecodeOptions",
33
33
  "EncodeOptions",
34
- "Undefined",
35
34
  ]
36
35
 
37
36
  from .decode import decode, load, loads
@@ -44,4 +43,3 @@ from .enums.list_format import ListFormat
44
43
  from .enums.sentinel import Sentinel
45
44
  from .models.decode_options import DecodeOptions
46
45
  from .models.encode_options import EncodeOptions
47
- from .models.undefined import Undefined
@@ -0,0 +1,50 @@
1
+ """Charset sentinel values and their percent-encoded forms.
2
+
3
+ Browsers sometimes include an ``utf8=…`` sentinel in
4
+ ``application/x-www-form-urlencoded`` submissions to signal the character
5
+ encoding that was used. This module exposes those sentinels as an enum whose
6
+ members carry both the raw token emitted by the page and the fully URL-encoded
7
+ fragment that appears on the wire.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+
13
+ __all__ = ("Sentinel",)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class _SentinelDataMixin:
18
+ """Common data carried by each charset sentinel.
19
+
20
+ Attributes:
21
+ raw: The unencoded token browsers start with, such as the HTML entity
22
+ string ``"&#10003;"`` or the literal check mark ``"✓"``.
23
+ encoded: The full URL-encoded ``key=value`` fragment, such as
24
+ ``"utf8=%26%2310003%3B"`` or ``"utf8=%E2%9C%93"``.
25
+ """
26
+
27
+ raw: str
28
+ encoded: str
29
+
30
+
31
+ class Sentinel(_SentinelDataMixin, Enum):
32
+ """Charset sentinels recognized and emitted by the codec.
33
+
34
+ Each member provides the source token through ``raw`` and the final
35
+ percent-encoded ``utf8=…`` fragment through ``encoded``.
36
+ """
37
+
38
+ ISO = r"&#10003;", r"utf8=%26%2310003%3B"
39
+ """HTML-entity sentinel used by non-UTF-8 submissions.
40
+
41
+ When a check mark (✓) appears but the form encoding is ``iso-8859-1`` or
42
+ another charset that lacks it, browsers HTML-entity-escape the character
43
+ and then URL-encode it, producing ``utf8=%26%2310003%3B``.
44
+ """
45
+
46
+ CHARSET = r"✓", r"utf8=%E2%9C%93"
47
+ """UTF-8 sentinel indicating that the request is UTF-8 encoded.
48
+
49
+ The encoded form contains the percent-encoded UTF-8 sequence for ✓.
50
+ """
@@ -0,0 +1,62 @@
1
+ """Internal undefined sentinel used while building codec data structures.
2
+
3
+ ``Undefined`` represents a missing value that should be omitted, similar to
4
+ JavaScript's ``undefined``. It is distinct from ``None``, which represents an
5
+ explicit null value.
6
+
7
+ The sentinel is identity-based: every construction returns the same instance,
8
+ allowing reliable ``is`` comparisons throughout the codec internals.
9
+ """
10
+
11
+ import threading
12
+ import typing as t
13
+
14
+ __all__ = ()
15
+
16
+
17
+ class Undefined:
18
+ """Singleton sentinel representing a value that should be omitted.
19
+
20
+ This is not equivalent to ``None``. The encoder and helper utilities use it
21
+ temporarily to mark absent entries, then remove those entries before
22
+ returning public results.
23
+ """
24
+
25
+ __slots__ = ()
26
+ _lock: t.ClassVar[threading.Lock] = threading.Lock()
27
+ _instance: t.ClassVar[t.Optional["Undefined"]] = None
28
+
29
+ def __new__(cls):
30
+ """Return the singleton instance.
31
+
32
+ Repeated construction returns the same object reference so identity
33
+ checks remain stable.
34
+ """
35
+ if cls._instance is None:
36
+ with cls._lock:
37
+ if cls._instance is None:
38
+ cls._instance = super().__new__(cls)
39
+ return cls._instance
40
+
41
+ def __repr__(self) -> str: # pragma: no cover - trivial
42
+ """Return a string representation of the singleton."""
43
+ return "Undefined()"
44
+
45
+ def __copy__(self): # pragma: no cover - trivial
46
+ """Preserve singleton identity when shallow-copied."""
47
+ return self
48
+
49
+ def __deepcopy__(self, memo): # pragma: no cover - trivial
50
+ """Preserve singleton identity when deep-copied."""
51
+ return self
52
+
53
+ def __reduce__(self): # pragma: no cover - trivial
54
+ """Reconstruct through the singleton constructor when unpickled."""
55
+ return Undefined, ()
56
+
57
+ def __init_subclass__(cls, **kwargs): # pragma: no cover - defensive
58
+ """Prevent subclassing to preserve singleton identity."""
59
+ raise TypeError("Undefined cannot be subclassed")
60
+
61
+
62
+ UNDEFINED: t.Final["Undefined"] = Undefined()
@@ -4,7 +4,7 @@
4
4
  "description": "A comparison of query string parsing libraries",
5
5
  "author": "Klemen Tusar",
6
6
  "license": "BSD-3-Clause",
7
- "packageManager": "pnpm@11.1.3",
7
+ "packageManager": "pnpm@11.5.2",
8
8
  "dependencies": {
9
9
  "qs": "^6.15.2"
10
10
  }
@@ -271,9 +271,6 @@ class TestEncoding:
271
271
  assert qs_codec.encode({"a": {"b": []}}) == ""
272
272
  assert qs_codec.encode({"a": {"b": {}}}) == ""
273
273
 
274
- # Properties that are `Undefined` will be omitted entirely:
275
- assert qs_codec.encode({"a": None, "b": qs_codec.Undefined()}) == "a="
276
-
277
274
  # The query string may optionally be prepended with a question mark:
278
275
  assert qs_codec.encode({"a": "b", "c": "d"}, qs_codec.EncodeOptions(add_query_prefix=True)) == "?a=b&c=d"
279
276
 
@@ -0,0 +1,18 @@
1
+ import qs_codec
2
+ import qs_codec.enums.sentinel as sentinel_module
3
+ import qs_codec.models.undefined as undefined_module
4
+
5
+
6
+ def test_sentinel_is_part_of_the_public_api():
7
+ assert "Sentinel" in qs_codec.__all__
8
+ assert qs_codec.Sentinel is sentinel_module.Sentinel
9
+
10
+
11
+ def test_undefined_is_not_part_of_the_public_api():
12
+ assert "Undefined" not in qs_codec.__all__
13
+ assert not hasattr(qs_codec, "Undefined")
14
+
15
+
16
+ def test_sentinel_modules_define_their_intended_exports():
17
+ assert sentinel_module.__all__ == ("Sentinel",)
18
+ assert undefined_module.__all__ == ()
@@ -1,50 +0,0 @@
1
- """Sentinel values and their percent-encoded forms.
2
-
3
- Browsers sometimes include an ``utf8=…`` “sentinel” in
4
- ``application/x-www-form-urlencoded`` submissions to signal the character
5
- encoding that was used. This module exposes those sentinels as an ``Enum``,
6
- where each member carries both the raw token (what the page emits) and the
7
- fully URL-encoded fragment (what appears on the wire).
8
- """
9
-
10
- from dataclasses import dataclass
11
- from enum import Enum
12
-
13
-
14
- @dataclass(frozen=True)
15
- class _SentinelDataMixin:
16
- """Common data carried by each sentinel.
17
-
18
- Attributes:
19
- raw: The unencoded token browsers start with. For example, the HTML
20
- entity string ``"&#10003;"`` or the literal check mark ``"✓"``.
21
- encoded: The full ``key=value`` fragment after URL-encoding, e.g.
22
- ``"utf8=%26%2310003%3B"`` or ``"utf8=%E2%9C%93"``.
23
- """
24
-
25
- raw: str
26
- encoded: str
27
-
28
-
29
- class Sentinel(_SentinelDataMixin, Enum):
30
- """All supported ``utf8`` sentinels.
31
-
32
- Each enum member provides:
33
- - ``raw``: the source token a browser starts with, and
34
- - ``encoded``: the final, percent-encoded ``utf8=…`` fragment.
35
- """
36
-
37
- ISO = r"&#10003;", r"utf8=%26%2310003%3B"
38
- """HTML-entity sentinel used by non-UTF-8 submissions.
39
-
40
- When a check mark (✓) appears but the page/form encoding is ``iso-8859-1``
41
- (or another charset that lacks ✓), browsers first HTML-entity-escape it as
42
- ``"&#10003;"`` and then URL-encode it, producing ``utf8=%26%2310003%3B``.
43
- """
44
-
45
- CHARSET = r"✓", r"utf8=%E2%9C%93"
46
- """UTF-8 sentinel indicating the request is UTF-8 encoded.
47
-
48
- This is the percent-encoded UTF-8 sequence for ✓, yielding the fragment
49
- ``utf8=%E2%9C%93``.
50
- """
@@ -1,73 +0,0 @@
1
- """Undefined sentinel.
2
-
3
- This module defines a tiny singleton `Undefined` used as a *sentinel* to mean “no value provided / omit this key”,
4
- similar to JavaScript’s `undefined`.
5
-
6
- Unlike `None` (which commonly means an explicit null), `Undefined` is used by the encoder and helper utilities to *skip*
7
- emitting a key or to signal that a value is intentionally absent and should not be serialized.
8
-
9
- The sentinel is identity-based: every construction returns the same instance, so `is` comparisons are reliable
10
- (e.g., `value is Undefined()`).
11
- """
12
-
13
- import threading
14
- import typing as t
15
-
16
-
17
- class Undefined:
18
- """Singleton sentinel object representing an “undefined” value.
19
-
20
- Notes:
21
- * This is **not** the same as `None`. Use `None` to represent a *null* value and `Undefined()` to represent “no value / omit”.
22
- * All calls to ``Undefined()`` return the same instance. Prefer identity checks (``is``) over equality checks.
23
-
24
- Examples:
25
- >>> from qs_codec.models.undefined import Undefined
26
- >>> a = Undefined()
27
- >>> b = Undefined()
28
- >>> a is b
29
- True
30
- >>> # Use it to indicate a key should be omitted when encoding:
31
- >>> maybe_value = Undefined()
32
- >>> if maybe_value is Undefined():
33
- ... pass # skip emitting the key
34
- """
35
-
36
- __slots__ = ()
37
- _lock: t.ClassVar[threading.Lock] = threading.Lock()
38
- _instance: t.ClassVar[t.Optional["Undefined"]] = None
39
-
40
- def __new__(cls):
41
- """Return the singleton instance.
42
-
43
- Creating `Undefined()` multiple times always returns the same object reference. This ensures identity checks (``is``) are stable.
44
- """
45
- if cls._instance is None:
46
- with cls._lock:
47
- if cls._instance is None:
48
- cls._instance = super().__new__(cls)
49
- return cls._instance
50
-
51
- def __repr__(self) -> str: # pragma: no cover - trivial
52
- """Return a string representation of the singleton."""
53
- return "Undefined()"
54
-
55
- def __copy__(self): # pragma: no cover - trivial
56
- """Ensure copies/pickles preserve the singleton identity."""
57
- return self
58
-
59
- def __deepcopy__(self, memo): # pragma: no cover - trivial
60
- """Ensure deep copies preserve the singleton identity."""
61
- return self
62
-
63
- def __reduce__(self): # pragma: no cover - trivial
64
- """Recreate via calling the constructor, which returns the singleton."""
65
- return Undefined, ()
66
-
67
- def __init_subclass__(cls, **kwargs): # pragma: no cover - defensive
68
- """Prevent subclassing of Undefined."""
69
- raise TypeError("Undefined cannot be subclassed")
70
-
71
-
72
- UNDEFINED: t.Final["Undefined"] = Undefined()
73
- __all__ = ["Undefined", "UNDEFINED"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes