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.
- {qs_codec-1.5.2 → qs_codec-1.6.0}/CHANGELOG.md +4 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/PKG-INFO +62 -9
- {qs_codec-1.5.2 → qs_codec-1.6.0}/README.rst +61 -8
- {qs_codec-1.5.2 → qs_codec-1.6.0}/docs/README.rst +61 -8
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/__init__.py +1 -3
- qs_codec-1.6.0/src/qs_codec/enums/sentinel.py +50 -0
- qs_codec-1.6.0/src/qs_codec/models/undefined.py +62 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/package.json +1 -1
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/example_test.py +0 -3
- qs_codec-1.6.0/tests/unit/package_test.py +18 -0
- qs_codec-1.5.2/src/qs_codec/enums/sentinel.py +0 -50
- qs_codec-1.5.2/src/qs_codec/models/undefined.py +0 -73
- {qs_codec-1.5.2 → qs_codec-1.6.0}/.gitignore +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/CODE-OF-CONDUCT.md +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/LICENSE +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/pyproject.toml +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/requirements_dev.txt +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/constants/__init__.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/constants/encode_constants.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/decode.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/encode.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/__init__.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/charset.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/decode_kind.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/duplicates.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/format.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/enums/list_format.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/__init__.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/cycle_state.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/decode_options.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/encode_frame.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/encode_options.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/key_path_node.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/overflow_dict.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/structured_key_scan.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/models/weak_wrapper.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/py.typed +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/utils/__init__.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/utils/decode_utils.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/utils/encode_utils.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/src/qs_codec/utils/utils.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/.gitignore +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/__init__.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/compare_outputs.sh +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/pnpm-lock.yaml +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/pnpm-workspace.yaml +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/qs.js +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/qs.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/comparison/test_cases.json +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/e2e/__init__.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/e2e/e2e_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/__init__.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/decode_options_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/decode_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/encode_internal_helpers_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/encode_options_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/encode_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/fixed_qs_issues_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/key_path_node_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/list_format_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/thread_safety_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/utils_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/weakref_test.py +0 -0
- {qs_codec-1.5.2 → qs_codec-1.6.0}/tests/unit/wpt_urlencoded_parser_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qs-codec
|
|
3
|
-
Version: 1.
|
|
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.
|
|
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 ``"✓"`` 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"✓", 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()
|
|
@@ -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 ``"✓"`` 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"✓", 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
|
-
``"✓"`` 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|