qs-codec 1.5.1__tar.gz → 1.5.2__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 (61) hide show
  1. {qs_codec-1.5.1 → qs_codec-1.5.2}/CHANGELOG.md +6 -0
  2. {qs_codec-1.5.1 → qs_codec-1.5.2}/PKG-INFO +1 -1
  3. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/__init__.py +1 -1
  4. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/decode.py +23 -22
  5. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/encode.py +71 -69
  6. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/encode_options.py +2 -3
  7. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/weak_wrapper.py +3 -3
  8. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/utils/decode_utils.py +31 -28
  9. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/utils/encode_utils.py +33 -35
  10. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/utils/utils.py +40 -39
  11. {qs_codec-1.5.1 → qs_codec-1.5.2}/.gitignore +0 -0
  12. {qs_codec-1.5.1 → qs_codec-1.5.2}/CODE-OF-CONDUCT.md +0 -0
  13. {qs_codec-1.5.1 → qs_codec-1.5.2}/LICENSE +0 -0
  14. {qs_codec-1.5.1 → qs_codec-1.5.2}/README.rst +0 -0
  15. {qs_codec-1.5.1 → qs_codec-1.5.2}/docs/README.rst +0 -0
  16. {qs_codec-1.5.1 → qs_codec-1.5.2}/pyproject.toml +0 -0
  17. {qs_codec-1.5.1 → qs_codec-1.5.2}/requirements_dev.txt +0 -0
  18. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/constants/__init__.py +0 -0
  19. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/constants/encode_constants.py +0 -0
  20. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/enums/__init__.py +0 -0
  21. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/enums/charset.py +0 -0
  22. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/enums/decode_kind.py +0 -0
  23. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/enums/duplicates.py +0 -0
  24. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/enums/format.py +0 -0
  25. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/enums/list_format.py +0 -0
  26. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/enums/sentinel.py +0 -0
  27. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/__init__.py +0 -0
  28. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/cycle_state.py +0 -0
  29. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/decode_options.py +0 -0
  30. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/encode_frame.py +0 -0
  31. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/key_path_node.py +0 -0
  32. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/overflow_dict.py +0 -0
  33. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/structured_key_scan.py +0 -0
  34. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/models/undefined.py +0 -0
  35. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/py.typed +0 -0
  36. {qs_codec-1.5.1 → qs_codec-1.5.2}/src/qs_codec/utils/__init__.py +0 -0
  37. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/.gitignore +0 -0
  38. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/__init__.py +0 -0
  39. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/compare_outputs.sh +0 -0
  40. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/package.json +0 -0
  41. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/pnpm-lock.yaml +0 -0
  42. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/pnpm-workspace.yaml +0 -0
  43. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/qs.js +0 -0
  44. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/qs.py +0 -0
  45. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/comparison/test_cases.json +0 -0
  46. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/e2e/__init__.py +0 -0
  47. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/e2e/e2e_test.py +0 -0
  48. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/__init__.py +0 -0
  49. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/decode_options_test.py +0 -0
  50. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/decode_test.py +0 -0
  51. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/encode_internal_helpers_test.py +0 -0
  52. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/encode_options_test.py +0 -0
  53. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/encode_test.py +0 -0
  54. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/example_test.py +0 -0
  55. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/fixed_qs_issues_test.py +0 -0
  56. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/key_path_node_test.py +0 -0
  57. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/list_format_test.py +0 -0
  58. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/thread_safety_test.py +0 -0
  59. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/utils_test.py +0 -0
  60. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/weakref_test.py +0 -0
  61. {qs_codec-1.5.1 → qs_codec-1.5.2}/tests/unit/wpt_urlencoded_parser_test.py +0 -0
@@ -1,3 +1,9 @@
1
+ ## 1.5.2
2
+
3
+ * [CHORE] simplify mapping and key-iteration checks in encode/decode internals
4
+ * [CHORE] improve readability in `EncodeOptions`, `Utils`, `DecodeUtils`, and `EncodeUtils` helper code
5
+ * [CHORE] expand internal type annotations across encode, decode, utils, and weak-wrapper helpers
6
+
1
7
  ## 1.5.1
2
8
 
3
9
  * [FEAT] add `DecodeOptions.strict_merge` for Node `qs` 6.15 `strictMerge` parity
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qs-codec
3
- Version: 1.5.1
3
+ Version: 1.5.2
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/
@@ -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.1"
17
+ __version__ = "1.5.2"
18
18
 
19
19
  # Public API surface re-exported at the package root.
20
20
  __all__ = [
@@ -73,12 +73,12 @@ def decode(
73
73
  if not isinstance(value, (str, Mapping)):
74
74
  raise ValueError("value must be a str or a Mapping[str, Any]")
75
75
 
76
- opts = options if options is not None else DecodeOptions()
77
- decode_from_string = isinstance(value, str)
76
+ opts: DecodeOptions = options if options is not None else DecodeOptions()
77
+ decode_from_string: bool = isinstance(value, str)
78
78
  str_value: str = t.cast(str, value) if decode_from_string else ""
79
79
  mapping_value: t.Mapping[str, t.Any] = t.cast(t.Mapping[str, t.Any], value) if not decode_from_string else {}
80
80
 
81
- parse_lists_effective = opts.parse_lists
81
+ parse_lists_effective: bool = opts.parse_lists
82
82
  if decode_from_string and parse_lists_effective:
83
83
  # Keep caller options immutable: compute a local parse_lists switch only for this invocation.
84
84
  query = str_value.replace("?", "", 1) if opts.ignore_query_prefix else str_value
@@ -96,7 +96,9 @@ def decode(
96
96
  if not temp_obj:
97
97
  return obj
98
98
 
99
- structured_scan = _scan_structured_keys(temp_obj, opts) if decode_from_string else StructuredKeyScan.empty()
99
+ structured_scan: StructuredKeyScan = (
100
+ _scan_structured_keys(temp_obj, opts) if decode_from_string else StructuredKeyScan.empty()
101
+ )
100
102
  if decode_from_string and not structured_scan.has_any_structured_syntax:
101
103
  return Utils.compact(temp_obj)
102
104
 
@@ -142,18 +144,18 @@ def loads(value: t.Optional[str], options: t.Optional[DecodeOptions] = None) ->
142
144
 
143
145
  def _first_structured_split_index(key: str, allow_dots: bool) -> int:
144
146
  """Return the earliest index that indicates structured syntax in ``key``."""
145
- split_at = key.find("[")
147
+ split_at: int = key.find("[")
146
148
  if not allow_dots:
147
149
  return split_at
148
150
 
149
- dot_index = key.find(".")
151
+ dot_index: int = key.find(".")
150
152
  if dot_index >= 0 and (split_at < 0 or dot_index < split_at):
151
153
  split_at = dot_index
152
154
 
153
- encoded_dot_index = -1
155
+ encoded_dot_index: int = -1
154
156
  if "%" in key:
155
- upper = key.find("%2E")
156
- lower = key.find("%2e")
157
+ upper: int = key.find("%2E")
158
+ lower: int = key.find("%2e")
157
159
  if upper >= 0 and lower >= 0:
158
160
  encoded_dot_index = upper if upper < lower else lower
159
161
  else:
@@ -167,7 +169,7 @@ def _first_structured_split_index(key: str, allow_dots: bool) -> int:
167
169
 
168
170
  def _leading_structured_root(key: str, options: DecodeOptions) -> str:
169
171
  """Extract root key for leading-bracket structured keys (``[]`` normalizes to ``"0"``)."""
170
- segments = DecodeUtils.split_key_into_segments(
172
+ segments: t.List[str] = DecodeUtils.split_key_into_segments(
171
173
  original_key=key,
172
174
  allow_dots=t.cast(bool, options.allow_dots),
173
175
  max_depth=options.depth,
@@ -190,12 +192,12 @@ def _scan_structured_keys(temp_obj: Mapping[str, t.Any], options: DecodeOptions)
190
192
  if not temp_obj:
191
193
  return StructuredKeyScan.empty()
192
194
 
193
- allow_dots = t.cast(bool, options.allow_dots)
195
+ allow_dots: bool = t.cast(bool, options.allow_dots)
194
196
  structured_roots: t.Set[str] = set()
195
197
  structured_keys: t.Set[str] = set()
196
198
 
197
- for key in temp_obj.keys():
198
- split_at = _first_structured_split_index(key, allow_dots)
199
+ for key in temp_obj:
200
+ split_at: int = _first_structured_split_index(key, allow_dots)
199
201
  if split_at < 0:
200
202
  continue
201
203
  structured_keys.add(key)
@@ -346,23 +348,22 @@ def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str
346
348
 
347
349
  # Local, non-optional decoder reference for type-checkers
348
350
  decoder_fn: t.Callable[..., t.Optional[str]] = options.decoder or DecodeUtils.decode
349
- duplicates = options.duplicates
351
+ duplicates: Duplicates = options.duplicates
350
352
 
351
353
  # Iterate over parts and decode each key/value pair.
352
- for i, _ in enumerate(parts):
354
+ for i, part in enumerate(parts):
353
355
  if i == skip_index:
354
356
  continue
355
357
 
356
- part: str = parts[i]
357
358
  if not part:
358
359
  continue
359
360
  bracket_equals_pos: int = part.find("]=")
360
361
  pos: int = part.find("=") if bracket_equals_pos == -1 else (bracket_equals_pos + 1)
361
- bracket_array_assignment = pos != -1 and "[]=" in part
362
+ bracket_array_assignment: bool = pos != -1 and "[]=" in part
362
363
 
363
364
  # Decode key and value with a key-aware decoder; skip pairs whose key decodes to None
364
- raw_key = ""
365
- list_limit_exceeded = False
365
+ raw_key: str = ""
366
+ list_limit_exceeded: bool = False
366
367
  if pos == -1:
367
368
  key_decoded = decoder_fn(part, charset, kind=DecodeKind.KEY)
368
369
  if key_decoded is None:
@@ -453,7 +454,7 @@ def _parse_object(
453
454
  handled by the splitter.
454
455
  - When list parsing is disabled and an empty segment is encountered, coerces to ``{"0": leaf}`` to preserve round-trippability with other ports.
455
456
  """
456
- parse_lists_enabled = options.parse_lists if parse_lists is None else parse_lists
457
+ parse_lists_enabled: bool = options.parse_lists if parse_lists is None else parse_lists
457
458
  current_list_length: int = 0
458
459
 
459
460
  # If the chain ends with an empty list marker, compute current list length for limit checks.
@@ -481,12 +482,12 @@ def _parse_object(
481
482
  if parent_key is not None and isinstance(val, (list, tuple)) and parent_key in dict(enumerate(val)):
482
483
  current_list_length = len(val[parent_key])
483
484
 
484
- bracket_array_comma_value = (
485
+ bracket_array_comma_value: bool = (
485
486
  not values_parsed
486
487
  and bool(chain)
487
488
  and chain[-1] == "[]"
488
489
  and isinstance(val, str)
489
- and val
490
+ and bool(val)
490
491
  and options.comma
491
492
  and "," in val
492
493
  )
@@ -64,28 +64,28 @@ def encode(value: t.Any, options: t.Optional[EncodeOptions] = None) -> str:
64
64
  if value is None:
65
65
  return ""
66
66
 
67
- opts = options if options is not None else EncodeOptions()
67
+ opts: EncodeOptions = options if options is not None else EncodeOptions()
68
68
 
69
- filter_opt = opts.filter
70
- filter_is_callable = callable(filter_opt)
71
- filter_is_sequence = (
69
+ filter_opt: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]] = opts.filter
70
+ filter_is_callable: bool = callable(filter_opt)
71
+ filter_is_sequence: bool = (
72
72
  filter_opt is not None
73
73
  and isinstance(filter_opt, ABCSequence)
74
74
  and not isinstance(filter_opt, (str, bytes, bytearray))
75
75
  )
76
- list_format = opts.list_format
77
- sort_opt = opts.sort if callable(opts.sort) else None
78
- encoder = opts.encoder if opts.encode else None
79
- formatter = opts.format.formatter
76
+ list_format: ListFormat = opts.list_format
77
+ sort_opt: t.Optional[t.Callable[[t.Any, t.Any], int]] = opts.sort if callable(opts.sort) else None
78
+ encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]] = (
79
+ opts.encoder if opts.encode else None
80
+ )
81
+ formatter: t.Optional[t.Callable[[str], str]] = opts.format.formatter
80
82
 
81
83
  # Normalize the root into a mapping we can traverse deterministically:
82
84
  # - Mapping -> shallow copy (deep-copy only when a callable filter may mutate)
83
85
  # - Sequence -> optionally deep-copy for callable filters, then promote to {"0": v0, "1": v1, ...}
84
86
  # - Other -> empty (encodes to "")
85
87
  obj: t.Mapping[str, t.Any]
86
- if isinstance(value, dict):
87
- obj = deepcopy(value) if filter_is_callable else dict(value)
88
- elif isinstance(value, ABCMapping):
88
+ if isinstance(value, ABCMapping):
89
89
  obj = deepcopy(value) if filter_is_callable else dict(value)
90
90
  elif isinstance(value, (list, tuple)):
91
91
  sequence = deepcopy(value) if filter_is_callable else value
@@ -123,17 +123,17 @@ def encode(value: t.Any, options: t.Optional[EncodeOptions] = None) -> str:
123
123
 
124
124
  # Side channel seed for legacy `_encode` compatibility (and cycle-state bootstrap when provided).
125
125
  side_channel: WeakKeyDictionary = WeakKeyDictionary()
126
- max_depth = _get_max_encode_depth(opts.max_depth)
126
+ max_depth: int = _get_max_encode_depth(opts.max_depth)
127
127
 
128
128
  # Encode each selected root key.
129
- missing = _MISSING
129
+ missing: t.Any = _MISSING
130
130
  for _key in obj_keys:
131
131
  if not isinstance(_key, str):
132
132
  # Skip non-string keys; parity with ports that stringify key paths.
133
133
  continue
134
134
 
135
- obj_value = obj.get(_key, missing)
136
- key_is_undefined = obj_value is missing
135
+ obj_value: t.Any = obj.get(_key, missing)
136
+ key_is_undefined: bool = obj_value is missing
137
137
 
138
138
  # Optionally drop explicit nulls at the root.
139
139
  if opts.skip_nulls and obj_value is None:
@@ -232,12 +232,12 @@ def _bootstrap_cycle_state_from_side_channel(side_channel: WeakKeyDictionary) ->
232
232
  from the current frame to the top-most side-channel mapping.
233
233
  """
234
234
  chain: t.List[WeakKeyDictionary] = []
235
- tmp_sc = side_channel.get(_sentinel)
235
+ tmp_sc: t.Any = side_channel.get(_sentinel)
236
236
  while isinstance(tmp_sc, WeakKeyDictionary):
237
237
  chain.append(tmp_sc)
238
238
  tmp_sc = tmp_sc.get(_sentinel) # type: ignore[assignment]
239
239
 
240
- state = CycleState()
240
+ state: CycleState = CycleState()
241
241
  for level, ancestor in enumerate(reversed(chain)):
242
242
  is_top = ancestor.get(_sentinel) is None
243
243
  for key, pos in ancestor.items():
@@ -257,8 +257,8 @@ def _compute_step_and_check_cycle(state: CycleState, node_key: t.Any, current_le
257
257
  * raise when ancestor_pos == distance
258
258
  * return 0 when no match or when nearest match is the top-most side-channel
259
259
  """
260
- key_id = node_key if isinstance(node_key, int) else _identity_key(node_key)
261
- entries = state.entries.get(key_id)
260
+ key_id: int = node_key if isinstance(node_key, int) else _identity_key(node_key)
261
+ entries: t.Optional[t.List[t.Tuple[int, int, bool]]] = state.entries.get(key_id)
262
262
  if not entries:
263
263
  return 0
264
264
 
@@ -271,13 +271,13 @@ def _compute_step_and_check_cycle(state: CycleState, node_key: t.Any, current_le
271
271
 
272
272
 
273
273
  def _push_current_node(state: CycleState, node_key: t.Any, current_level: int, pos: int, is_top: bool) -> None:
274
- key_id = node_key if isinstance(node_key, int) else _identity_key(node_key)
274
+ key_id: int = node_key if isinstance(node_key, int) else _identity_key(node_key)
275
275
  state.entries.setdefault(key_id, []).append((current_level, pos, is_top))
276
276
 
277
277
 
278
278
  def _pop_current_node(state: CycleState, node_key: t.Any) -> None:
279
- key_id = node_key if isinstance(node_key, int) else _identity_key(node_key)
280
- entries = state.entries.get(key_id)
279
+ key_id: int = node_key if isinstance(node_key, int) else _identity_key(node_key)
280
+ entries: t.Optional[t.List[t.Tuple[int, int, bool]]] = state.entries.get(key_id)
281
281
  if not entries:
282
282
  return
283
283
 
@@ -286,10 +286,10 @@ def _pop_current_node(state: CycleState, node_key: t.Any) -> None:
286
286
  del state.entries[key_id]
287
287
 
288
288
 
289
- _INDICES_GENERATOR = ListFormat.INDICES.generator
290
- _BRACKETS_GENERATOR = ListFormat.BRACKETS.generator
291
- _REPEAT_GENERATOR = ListFormat.REPEAT.generator
292
- _COMMA_GENERATOR = ListFormat.COMMA.generator
289
+ _INDICES_GENERATOR: t.Callable[[str, t.Optional[str]], str] = ListFormat.INDICES.generator
290
+ _BRACKETS_GENERATOR: t.Callable[[str, t.Optional[str]], str] = ListFormat.BRACKETS.generator
291
+ _REPEAT_GENERATOR: t.Callable[[str, t.Optional[str]], str] = ListFormat.REPEAT.generator
292
+ _COMMA_GENERATOR: t.Callable[[str, t.Optional[str]], str] = ListFormat.COMMA.generator
293
293
 
294
294
 
295
295
  def _uses_legacy_cycle_state(side_channel: WeakKeyDictionary) -> bool:
@@ -380,19 +380,19 @@ def _try_encode_linear_chain_plain_dict(
380
380
  ):
381
381
  return None
382
382
 
383
- current = value
383
+ current: t.Any = value
384
384
  if type(current) is not dict: # pylint: disable=unidiomatic-typecheck
385
385
  return None
386
386
 
387
- current_depth = _depth
388
- max_depth = _get_max_encode_depth(_max_depth)
389
- formatter_fn = formatter if formatter is not None else format.formatter
390
- prefix_value = prefix if prefix is not None else ("?" if add_query_prefix else "")
387
+ current_depth: int = _depth
388
+ max_depth: int = _get_max_encode_depth(_max_depth)
389
+ formatter_fn: t.Callable[[str], str] = formatter if formatter is not None else format.formatter
390
+ prefix_value: str = prefix if prefix is not None else ("?" if add_query_prefix else "")
391
391
  path_parts: t.List[str] = [prefix_value]
392
392
  # Match the generic walker so bounded cyclic payloads still let `max_depth`
393
393
  # win before the eventual cycle error on the same shapes.
394
- cycle_state = CycleState()
395
- root_level = _depth
394
+ cycle_state: CycleState = CycleState()
395
+ root_level: int = _depth
396
396
 
397
397
  while True:
398
398
  if current_depth > max_depth:
@@ -416,11 +416,11 @@ def _try_encode_linear_chain_plain_dict(
416
416
  continue
417
417
 
418
418
  if current is None:
419
- path = formatter_fn("".join(path_parts))
419
+ path: str = formatter_fn("".join(path_parts))
420
420
  return [f"{path}="]
421
421
 
422
422
  if current_type is bool:
423
- value_str = "true" if current else "false"
423
+ value_str: str = "true" if current else "false"
424
424
  path = formatter_fn("".join(path_parts))
425
425
  return [f"{path}={formatter_fn(value_str)}"]
426
426
 
@@ -429,7 +429,7 @@ def _try_encode_linear_chain_plain_dict(
429
429
  return [f"{path}={formatter_fn(str(current))}"]
430
430
 
431
431
  if current_type is datetime:
432
- date_value = t.cast(datetime, current)
432
+ date_value: datetime = t.cast(datetime, current)
433
433
  current = serialize_date(date_value) if callable(serialize_date) else date_value.isoformat()
434
434
  continue
435
435
 
@@ -489,19 +489,19 @@ def _try_encode_linear_chain(
489
489
  ):
490
490
  return None
491
491
 
492
- current = value
492
+ current: t.Any = value
493
493
  if type(current) is not dict and not isinstance(current, ABCMapping): # pylint: disable=unidiomatic-typecheck
494
494
  return None
495
495
 
496
- current_depth = _depth
497
- max_depth = _get_max_encode_depth(_max_depth)
498
- formatter_fn = formatter if formatter is not None else format.formatter
499
- prefix_value = prefix if prefix is not None else ("?" if add_query_prefix else "")
496
+ current_depth: int = _depth
497
+ max_depth: int = _get_max_encode_depth(_max_depth)
498
+ formatter_fn: t.Callable[[str], str] = formatter if formatter is not None else format.formatter
499
+ prefix_value: str = prefix if prefix is not None else ("?" if add_query_prefix else "")
500
500
  path_segments: t.List[str] = []
501
501
  # Match the generic walker so bounded cyclic payloads still let `max_depth`
502
502
  # win before the eventual cycle error on the same shapes.
503
- cycle_state = CycleState()
504
- root_level = _depth
503
+ cycle_state: CycleState = CycleState()
504
+ root_level: int = _depth
505
505
 
506
506
  while True:
507
507
  if current_depth > max_depth:
@@ -510,11 +510,11 @@ def _try_encode_linear_chain(
510
510
  current_type = type(current)
511
511
 
512
512
  if current is None:
513
- path = prefix_value + "".join(path_segments)
513
+ path: str = prefix_value + "".join(path_segments)
514
514
  return [f"{formatter_fn(path)}="]
515
515
 
516
516
  if current_type is bool:
517
- value_str = "true" if current else "false"
517
+ value_str: str = "true" if current else "false"
518
518
  path = prefix_value + "".join(path_segments)
519
519
  return [f"{formatter_fn(path)}={formatter_fn(value_str)}"]
520
520
 
@@ -524,7 +524,7 @@ def _try_encode_linear_chain(
524
524
  return [f"{formatter_fn(path)}={formatter_fn(value_str)}"]
525
525
 
526
526
  if current_type is datetime:
527
- date_value = t.cast(datetime, current)
527
+ date_value: datetime = t.cast(datetime, current)
528
528
  current = serialize_date(date_value) if callable(serialize_date) else date_value.isoformat()
529
529
  continue
530
530
 
@@ -546,8 +546,8 @@ def _try_encode_linear_chain(
546
546
  else:
547
547
  return None
548
548
 
549
- current_id = id(mapping)
550
- step = _compute_step_and_check_cycle(cycle_state, current_id, current_depth)
549
+ current_id: int = id(mapping)
550
+ step: int = _compute_step_and_check_cycle(cycle_state, current_id, current_depth)
551
551
 
552
552
  if len(mapping) != 1:
553
553
  return None
@@ -576,8 +576,8 @@ def _next_path_for_sequence(
576
576
  if generator is _REPEAT_GENERATOR or generator is _COMMA_GENERATOR:
577
577
  return path
578
578
 
579
- parent = path.materialize()
580
- child = generator(parent, encoded_key)
579
+ parent: str = path.materialize()
580
+ child: str = generator(parent, encoded_key)
581
581
  if child.startswith(parent):
582
582
  return path.append(child[len(parent) :])
583
583
 
@@ -649,7 +649,7 @@ def _encode(
649
649
  Returns:
650
650
  Either a list/tuple of tokens or a single token string.
651
651
  """
652
- fast_path_result = _try_encode_linear_chain_plain_dict(
652
+ fast_path_result: t.Optional[t.List[t.Any]] = _try_encode_linear_chain_plain_dict(
653
653
  value=value,
654
654
  is_undefined=is_undefined,
655
655
  side_channel=side_channel,
@@ -716,7 +716,7 @@ def _encode(
716
716
  if last_bracket_key == encoded_key and last_bracket_segment is not None:
717
717
  return last_bracket_segment
718
718
 
719
- segment = f"[{encoded_key}]"
719
+ segment: str = f"[{encoded_key}]"
720
720
  last_bracket_key = encoded_key
721
721
  last_bracket_segment = segment
722
722
  return segment
@@ -779,7 +779,7 @@ def _encode(
779
779
  ]
780
780
 
781
781
  while stack:
782
- frame = stack[-1]
782
+ frame: EncodeFrame = stack[-1]
783
783
 
784
784
  if frame.phase == PHASE_START:
785
785
  if frame.max_depth is None:
@@ -791,7 +791,7 @@ def _encode(
791
791
  if frame.prefix is None:
792
792
  frame.prefix = "?" if frame.add_query_prefix else ""
793
793
  frame.path = KeyPathNode.from_materialized(frame.prefix)
794
- current_path = frame.path
794
+ current_path: KeyPathNode = frame.path
795
795
  if current_path is None: # pragma: no cover - internal invariant
796
796
  raise RuntimeError("path is not initialized") # noqa: TRY003
797
797
  if frame.comma_round_trip is None:
@@ -800,8 +800,8 @@ def _encode(
800
800
  frame.formatter = frame.format.formatter
801
801
 
802
802
  obj: t.Any = frame.value
803
- filter_opt = frame.filter_
804
- filter_is_sequence = (
803
+ filter_opt: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]] = frame.filter_
804
+ filter_is_sequence: bool = (
805
805
  filter_opt is not None
806
806
  and isinstance(filter_opt, ABCSequence)
807
807
  and not isinstance(filter_opt, (str, bytes, bytearray))
@@ -820,13 +820,13 @@ def _encode(
820
820
 
821
821
  if not frame.is_undefined and obj is None:
822
822
  if frame.strict_null_handling:
823
- key_text = current_path.materialize()
824
- key_value = (
823
+ key_text: str = current_path.materialize()
824
+ key_value: str = (
825
825
  frame.encoder(key_text, frame.charset, frame.format)
826
826
  if callable(frame.encoder) and not frame.encode_values_only
827
827
  else key_text
828
828
  )
829
- result_token = frame.formatter(key_value) if frame.formatter is not None else key_value
829
+ result_token: str = frame.formatter(key_value) if frame.formatter is not None else key_value
830
830
  stack.pop()
831
831
  last_result = result_token
832
832
  continue
@@ -863,7 +863,7 @@ def _encode(
863
863
  last_result = []
864
864
  continue
865
865
 
866
- obj_id = id(obj)
866
+ obj_id: int = id(frame.obj)
867
867
  frame.obj_id = obj_id
868
868
  if use_legacy_cycle_state:
869
869
  if frame.cycle_state is None or frame.cycle_level is None:
@@ -884,7 +884,7 @@ def _encode(
884
884
 
885
885
  if frame.encode_values_only and callable(frame.encoder):
886
886
  encoded_items = [frame.encoder(item, frame.charset, frame.format) for item in comma_items]
887
- obj_keys_value = ",".join("" if e is None else str(e) for e in encoded_items)
887
+ obj_keys_value: str = ",".join("" if e is None else str(e) for e in encoded_items)
888
888
  else:
889
889
  obj_keys_value = ",".join(Utils.normalize_comma_elem(e) for e in comma_items)
890
890
 
@@ -893,20 +893,20 @@ def _encode(
893
893
  else:
894
894
  frame.obj_keys = [{"value": UNDEFINED}]
895
895
  elif filter_is_sequence:
896
- filter_keys = t.cast(t.Sequence[t.Union[str, int]], filter_opt)
896
+ filter_keys: t.Sequence[t.Union[str, int]] = t.cast(t.Sequence[t.Union[str, int]], filter_opt)
897
897
  frame.obj_keys = list(filter_keys)
898
898
  else:
899
899
  if frame.is_mapping:
900
- keys = list(obj) if isinstance(obj, dict) else list(obj.keys())
900
+ keys: t.List[t.Any] = list(obj) if isinstance(obj, dict) else list(obj.keys())
901
901
  elif frame.is_sequence:
902
902
  keys = list(range(len(obj)))
903
903
  else:
904
904
  keys = []
905
905
  frame.obj_keys = sorted(keys, key=cmp_to_key(frame.sort)) if frame.sort is not None else keys
906
906
 
907
- path_for_children = current_path.as_dot_encoded() if frame.encode_dot_in_keys else current_path
907
+ path_for_children: KeyPathNode = current_path.as_dot_encoded() if frame.encode_dot_in_keys else current_path
908
908
 
909
- single_item_for_round_trip = False
909
+ single_item_for_round_trip: bool = False
910
910
  if frame.comma_round_trip and frame.is_sequence:
911
911
  if frame.generate_array_prefix is _COMMA_GENERATOR and comma_effective_length is not None:
912
912
  single_item_for_round_trip = comma_effective_length == 1
@@ -952,7 +952,7 @@ def _encode(
952
952
  _push_current_node(public_cycle_state, frame.obj_id, frame.depth, frame.step, frame.depth == 0)
953
953
  frame.cycle_pushed = True
954
954
 
955
- _key = frame.obj_keys[frame.index]
955
+ _key: t.Any = frame.obj_keys[frame.index]
956
956
  frame.index += 1
957
957
 
958
958
  _value: t.Any
@@ -988,13 +988,15 @@ def _encode(
988
988
  continue
989
989
 
990
990
  key_text = str(_key)
991
- encoded_key = key_text.replace(".", "%2E") if frame.allow_dots and frame.encode_dot_in_keys else key_text
991
+ encoded_key: str = (
992
+ key_text.replace(".", "%2E") if frame.allow_dots and frame.encode_dot_in_keys else key_text
993
+ )
992
994
  if frame.path is None: # pragma: no cover - internal invariant
993
995
  raise RuntimeError("path is not initialized") # noqa: TRY003
994
- adjusted_path = frame.adjusted_path if frame.adjusted_path is not None else frame.path
996
+ adjusted_path: KeyPathNode = frame.adjusted_path if frame.adjusted_path is not None else frame.path
995
997
 
996
998
  if frame.is_sequence:
997
- child_path = _next_path_for_sequence(
999
+ child_path: KeyPathNode = _next_path_for_sequence(
998
1000
  adjusted_path,
999
1001
  frame.generate_array_prefix,
1000
1002
  encoded_key,
@@ -166,9 +166,8 @@ class EncodeOptions:
166
166
  name = f.name
167
167
  if name == "encoder":
168
168
  continue
169
- else:
170
- v1 = getattr(self, name)
171
- v2 = getattr(other, name)
169
+ v1 = getattr(self, name)
170
+ v2 = getattr(other, name)
172
171
  if v1 != v2:
173
172
  return False
174
173
  return True
@@ -31,9 +31,9 @@ _proxy_cache_lock = RLock()
31
31
 
32
32
  def _get_proxy(value: t.Any) -> "_Proxy":
33
33
  """Return a per-object proxy, cached by id(value)."""
34
- key = id(value)
34
+ key: int = id(value)
35
35
  with _proxy_cache_lock:
36
- proxy = _proxy_cache.get(key)
36
+ proxy: t.Optional[_Proxy] = _proxy_cache.get(key)
37
37
  if proxy is None:
38
38
  proxy = _Proxy(value)
39
39
  _proxy_cache[key] = proxy
@@ -54,7 +54,7 @@ class WeakWrapper:
54
54
 
55
55
  def __init__(self, value: t.Any) -> None:
56
56
  """Initialize the wrapper with a value."""
57
- proxy = _get_proxy(value)
57
+ proxy: _Proxy = _get_proxy(value)
58
58
  # Strong edge: wrapper -> proxy
59
59
  object.__setattr__(self, "_proxy", proxy)
60
60
  # Weak edge so tests can observe GC of the proxy
@@ -4,7 +4,11 @@ This mirrors the semantics of the Node `qs` library:
4
4
 
5
5
  - Decoding handles both UTF-8 and Latin-1 code paths.
6
6
  - Key splitting keeps bracket groups *balanced* and optionally treats dots as path separators when ``allow_dots=True``.
7
- - Top-level dot splitting uses a character-scanner that handles degenerate cases (leading '.' starts a bracket segment; '.[' is skipped; double dots preserve the first; trailing '.' is preserved) and never treats literal percent-encoded sequences (e.g., '%2E') as split points; only actual '.' characters at depth 0 are split.
7
+ - Top-level dot splitting uses a character-scanner that handles degenerate cases
8
+ (leading '.' starts a bracket segment; '.[' is skipped; double dots preserve
9
+ the first; trailing '.' is preserved) and never treats literal percent-encoded
10
+ sequences (e.g., '%2E') as split points; only actual '.' characters at depth
11
+ 0 are split.
8
12
  """
9
13
 
10
14
  import re
@@ -62,70 +66,70 @@ class DecodeUtils:
62
66
  """
63
67
  if "." not in s:
64
68
  return s
65
- sb: t.List[str] = []
66
- depth = 0
67
- i = 0
68
- n = len(s)
69
+ buffer: t.List[str] = []
70
+ depth: int = 0
71
+ i: int = 0
72
+ n: int = len(s)
69
73
  while i < n:
70
- ch = s[i]
74
+ ch: str = s[i]
71
75
  if ch == "[":
72
76
  depth += 1
73
- sb.append(ch)
77
+ buffer.append(ch)
74
78
  i += 1
75
79
  elif ch == "]":
76
80
  if depth > 0:
77
81
  depth -= 1
78
- sb.append(ch)
82
+ buffer.append(ch)
79
83
  i += 1
80
84
  elif ch == ".":
81
85
  if depth == 0:
82
- has_next = i + 1 < n
83
- next_ch = s[i + 1] if has_next else "\0"
86
+ has_next: bool = i + 1 < n
87
+ next_ch: str = s[i + 1] if has_next else "\0"
84
88
  if next_ch == "[":
85
89
  # skip the dot so 'a.[b]' acts like 'a[b]'
86
90
  i += 1
87
91
  elif next_ch == "]":
88
92
  # preserve ambiguous '.]' as a literal to avoid constructing '[]]'
89
- sb.append(".")
93
+ buffer.append(".")
90
94
  i += 1
91
95
  elif i == 0:
92
96
  # If input starts with '..', preserve the first dot like the 'a..b' case.
93
97
  if has_next and next_ch == ".":
94
- sb.append(".")
98
+ buffer.append(".")
95
99
  i += 1
96
100
  continue
97
101
  # leading '.' starts a bracket segment: ".a" -> "[a]"
98
102
  start = i + 1
99
- j = start
100
- while j < n and s[j] != "." and s[j] != "[" and s[j] != "]":
103
+ j: int = start
104
+ while j < n and s[j] not in ".[]":
101
105
  j += 1
102
- sb.append("[")
103
- sb.append(s[start:j])
104
- sb.append("]")
106
+ buffer.append("[")
107
+ buffer.append(s[start:j])
108
+ buffer.append("]")
105
109
  i = j
106
110
  elif (not has_next) or next_ch == ".":
107
111
  # trailing dot, or first of a double dot
108
- sb.append(".")
112
+ buffer.append(".")
109
113
  i += 1
110
114
  else:
111
115
  # normal split at top level: convert a.b → a[b]
112
116
  start = i + 1
113
117
  j = start
114
- while j < n and s[j] != "." and s[j] != "[" and s[j] != "]":
118
+ while j < n and s[j] not in ".[]":
115
119
  j += 1
116
- sb.append("[")
117
- sb.append(s[start:j])
118
- sb.append("]")
120
+ buffer.append("[")
121
+ buffer.append(s[start:j])
122
+ buffer.append("]")
119
123
  i = j
120
124
  else:
121
- sb.append(".")
125
+ buffer.append(".")
122
126
  i += 1
123
127
  else:
124
128
  # No special handling for percent sequences here; characters are appended as-is.
125
129
  # We never split on '%2E' at this stage.
126
- sb.append(ch)
130
+ buffer.append(ch)
127
131
  i += 1
128
- return "".join(sb)
132
+ return "".join(buffer)
129
133
 
130
134
  # Precompiled pattern for %XX hex bytes (Latin-1 path fast path)
131
135
  HEX2_PATTERN: t.Pattern[str] = re.compile(r"%([0-9A-Fa-f]{2})")
@@ -153,7 +157,7 @@ class DecodeUtils:
153
157
  def replacer(match: t.Match[str]) -> str:
154
158
  if (unicode_val := match.group("unicode")) is not None:
155
159
  return chr(int(unicode_val, 16))
156
- elif (hex_val := match.group("hex")) is not None:
160
+ if (hex_val := match.group("hex")) is not None:
157
161
  return chr(int(hex_val, 16))
158
162
  return match.group(0)
159
163
 
@@ -199,8 +203,7 @@ class DecodeUtils:
199
203
  _int, _chr = int, chr
200
204
  return cls.HEX2_PATTERN.sub(lambda m: _chr(_int(m.group(1), 16)), s)
201
205
 
202
- s = string_without_plus
203
- return s if "%" not in s else unquote(s)
206
+ return string_without_plus if "%" not in string_without_plus else unquote(string_without_plus)
204
207
 
205
208
  @classmethod
206
209
  def split_key_into_segments(
@@ -46,24 +46,28 @@ class EncodeUtils:
46
46
  ) -> str:
47
47
  """Emulate the legacy JavaScript escaping behavior.
48
48
 
49
- This function operates on UTF-16 *code units* to emulate JavaScript's legacy `%uXXXX` behavior. Non-BMP code
50
- points are first expanded into surrogate pairs via `_to_surrogates`, then each code unit is processed.
49
+ This function operates on UTF-16 *code units* to emulate JavaScript's legacy `%uXXXX` behavior.
50
+ Non-BMP code points are first expanded into surrogate pairs via `_to_surrogates`, then each code unit is
51
+ processed.
51
52
 
52
- - Safe set: when `format == Format.RFC1738`, the characters `(` and `)` are additionally treated as safe. Otherwise, the RFC3986 safe set is used.
53
+ - Safe set: when `format == Format.RFC1738`, the characters `(` and `)` are additionally treated as safe.
54
+ Otherwise, the RFC3986 safe set is used.
53
55
  - ASCII characters in the safe set are emitted unchanged.
54
56
  - Code units &lt; 256 are emitted as `%XX`.
55
57
  - Other code units are emitted as `%uXXXX`.
56
58
 
57
59
  Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/escape
58
60
  """
59
- safe_points_ascii = cls.RFC1738_SAFE_POINTS_ASCII if format == Format.RFC1738 else cls.SAFE_POINTS_ASCII
61
+ safe_points_ascii: t.Tuple[bool, ...] = (
62
+ cls.RFC1738_SAFE_POINTS_ASCII if format == Format.RFC1738 else cls.SAFE_POINTS_ASCII
63
+ )
60
64
  if string.isascii() and cls._all_ascii_safe(string, safe_points_ascii):
61
65
  return string
62
66
 
63
67
  if not string.isascii():
64
68
  string = cls._to_surrogates(string)
65
69
 
66
- hex_table = cls.HEX_TABLE
70
+ hex_table: t.Tuple[str, ...] = cls.HEX_TABLE
67
71
 
68
72
  buffer: t.List[str] = []
69
73
 
@@ -112,9 +116,9 @@ class EncodeUtils:
112
116
  return ""
113
117
 
114
118
  if charset == Charset.LATIN1:
115
- _pat = cls._RE_UXXXX
116
- _esc = cls.escape(string, format)
117
- return _pat.sub(lambda m: f"%26%23{int(m.group(1), 16)}%3B", _esc)
119
+ u_escape_pattern: t.Pattern[str] = cls._RE_UXXXX
120
+ escaped: str = cls.escape(string, format)
121
+ return u_escape_pattern.sub(lambda m: f"%26%23{int(m.group(1), 16)}%3B", escaped)
118
122
 
119
123
  return cls._encode_string(string, format)
120
124
 
@@ -129,20 +133,19 @@ class EncodeUtils:
129
133
  """
130
134
  if isinstance(value, bytes):
131
135
  return value.decode("utf-8")
132
- elif isinstance(value, bool):
136
+ if isinstance(value, bool):
133
137
  return str(value).lower()
134
- elif isinstance(value, str):
138
+ if isinstance(value, str):
135
139
  return value
136
- else:
137
- return str(value)
140
+ return str(value)
138
141
 
139
142
  @classmethod
140
143
  def _encode_string(cls, string: str, format: t.Optional[Format]) -> str:
141
144
  """Percent-encode `string` per RFC3986 or RFC1738, operating on UTF-16 code units.
142
145
 
143
- We first expand non-BMP code points into surrogate pairs so that indexing and length checks are done in *code units*,
144
- matching JavaScript semantics. We then walk the string with a manual index, skipping the low surrogate when we emit a
145
- surrogate pair.
146
+ We first expand non-BMP code points into surrogate pairs so that indexing and length checks are done in
147
+ *code units*, matching JavaScript semantics. We then walk the string with a manual index, skipping the low
148
+ surrogate when we emit a surrogate pair.
146
149
  """
147
150
  safe_chars_ascii = cls.RFC1738_SAFE_CHARS_ASCII if format == Format.RFC1738 else cls.SAFE_CHARS_ASCII
148
151
  if string.isascii():
@@ -152,7 +155,7 @@ class EncodeUtils:
152
155
  else:
153
156
  s = cls._to_surrogates(string)
154
157
 
155
- hex_table = cls.HEX_TABLE
158
+ hex_table: t.Tuple[str, ...] = cls.HEX_TABLE
156
159
  buffer: t.List[str] = []
157
160
 
158
161
  i = 0
@@ -214,48 +217,43 @@ class EncodeUtils:
214
217
  """
215
218
  if c < 0x80: # ASCII
216
219
  return [cls.HEX_TABLE[c]]
217
- elif c < 0x800: # 2 bytes
220
+ if c < 0x800: # 2 bytes
218
221
  return [
219
222
  cls.HEX_TABLE[0xC0 | (c >> 6)],
220
223
  cls.HEX_TABLE[0x80 | (c & 0x3F)],
221
224
  ]
222
- elif c < 0xD800 or c >= 0xE000: # 3 bytes
225
+ if c < 0xD800 or c >= 0xE000: # 3 bytes
223
226
  return [
224
227
  cls.HEX_TABLE[0xE0 | (c >> 12)],
225
228
  cls.HEX_TABLE[0x80 | ((c >> 6) & 0x3F)],
226
229
  cls.HEX_TABLE[0x80 | (c & 0x3F)],
227
230
  ]
228
- else:
229
- return cls._encode_surrogate_pair(string, i, c)
231
+ return cls._encode_surrogate_pair(string, i, c)
230
232
 
231
233
  @classmethod
232
234
  def _encode_surrogate_pair(cls, string: str, i: int, c: int) -> t.List[str]:
233
235
  """Encode a surrogate pair starting at `i` as a 4-byte UTF-8 sequence."""
234
- buffer: t.List[str] = []
235
236
  low = ord(string[i + 1])
236
237
  c = 0x10000 + (((c & 0x3FF) << 10) | (low & 0x3FF))
237
- buffer.extend(
238
- [
239
- cls.HEX_TABLE[0xF0 | (c >> 18)],
240
- cls.HEX_TABLE[0x80 | ((c >> 12) & 0x3F)],
241
- cls.HEX_TABLE[0x80 | ((c >> 6) & 0x3F)],
242
- cls.HEX_TABLE[0x80 | (c & 0x3F)],
243
- ],
244
- )
245
- return buffer
238
+ return [
239
+ cls.HEX_TABLE[0xF0 | (c >> 18)],
240
+ cls.HEX_TABLE[0x80 | ((c >> 12) & 0x3F)],
241
+ cls.HEX_TABLE[0x80 | ((c >> 6) & 0x3F)],
242
+ cls.HEX_TABLE[0x80 | (c & 0x3F)],
243
+ ]
246
244
 
247
245
  @staticmethod
248
246
  def _to_surrogates(string: str) -> str:
249
247
  """Expand non-BMP code points (code point &gt; 0xFFFF) into UTF-16 surrogate pairs.
250
248
 
251
249
  This mirrors how JavaScript strings store characters, allowing compatibility with legacy `%uXXXX` encoding paths
252
- and consistent behavior with the Node `qs` implementation. If no non-BMP
253
- code point is present, the original string is returned unchanged.
250
+ and consistent behavior with the Node `qs` implementation. If no non-BMP code point is present, the original
251
+ string is returned unchanged.
254
252
  """
255
253
  buffer: t.Optional[t.List[str]] = None
256
254
 
257
255
  for index, ch in enumerate(string):
258
- cp = ord(ch)
256
+ cp: int = ord(ch)
259
257
  if cp <= 0xFFFF:
260
258
  if buffer is not None:
261
259
  buffer.append(ch)
@@ -266,8 +264,8 @@ class EncodeUtils:
266
264
 
267
265
  # Convert to surrogate pair.
268
266
  cp -= 0x10000
269
- high = 0xD800 + (cp >> 10)
270
- low = 0xDC00 + (cp & 0x3FF)
267
+ high: int = 0xD800 + (cp >> 10)
268
+ low: int = 0xDC00 + (cp & 0x3FF)
271
269
  buffer.append(chr(high))
272
270
  buffer.append(chr(low))
273
271
 
@@ -38,7 +38,7 @@ def _numeric_key_pairs(mapping: t.Mapping[t.Any, t.Any]) -> t.List[t.Tuple[int,
38
38
  may overwrite earlier values when materializing numeric-keyed dicts.
39
39
  """
40
40
  pairs: t.List[t.Tuple[int, t.Any]] = []
41
- for key in mapping.keys():
41
+ for key in mapping:
42
42
  try:
43
43
  numeric_key = int(key)
44
44
  except (TypeError, ValueError):
@@ -125,17 +125,17 @@ class Utils:
125
125
  The merged structure. May be the original `target` object when
126
126
  `source` is ``None``.
127
127
  """
128
- opts = options if options is not None else DecodeOptions()
128
+ opts: DecodeOptions = options if options is not None else DecodeOptions()
129
129
  last_result: t.Any = None
130
130
 
131
131
  stack: t.List[_MergeFrame] = [_MergeFrame(target=target, source=source, options=opts)]
132
132
 
133
133
  while stack:
134
- frame = stack[-1]
134
+ frame: _MergeFrame = stack[-1]
135
135
 
136
136
  if frame.phase == "start":
137
- current_target = frame.target
138
- current_source = frame.source
137
+ current_target: t.Any = frame.target
138
+ current_source: t.Any = frame.source
139
139
 
140
140
  if current_source is None:
141
141
  stack.pop()
@@ -148,24 +148,26 @@ class Utils:
148
148
  # If the target sequence contains `Undefined`, we may need to promote it
149
149
  # to a dict keyed by indices for stable writes.
150
150
  if any(isinstance(el, Undefined) for el in current_target):
151
- target_: t.Dict[int, t.Any] = dict(enumerate(current_target))
151
+ target_by_index: t.Dict[int, t.Any] = dict(enumerate(current_target))
152
152
 
153
153
  if isinstance(current_source, (list, tuple)):
154
154
  for i, item in enumerate(current_source):
155
155
  if not isinstance(item, Undefined):
156
- target_[i] = item
156
+ target_by_index[i] = item
157
157
  else:
158
- target_[len(target_)] = current_source
158
+ target_by_index[len(target_by_index)] = current_source
159
159
 
160
160
  # When list parsing is disabled, collapse to a string-keyed dict and drop sentinels.
161
161
  if not frame.options.parse_lists and any(
162
- isinstance(value, Undefined) for value in target_.values()
162
+ isinstance(value, Undefined) for value in target_by_index.values()
163
163
  ):
164
164
  result: t.Any = {
165
- str(i): target_[i] for i in target_ if not isinstance(target_[i], Undefined)
165
+ str(i): target_by_index[i]
166
+ for i in target_by_index
167
+ if not isinstance(target_by_index[i], Undefined)
166
168
  }
167
169
  else:
168
- result = [el for el in target_.values() if not isinstance(el, Undefined)]
170
+ result = [el for el in target_by_index.values() if not isinstance(el, Undefined)]
169
171
  stack.pop()
170
172
  last_result = result
171
173
  continue
@@ -182,7 +184,7 @@ class Utils:
182
184
  frame.phase = "list_iter"
183
185
  continue
184
186
 
185
- mutable_target = (
187
+ mutable_target: t.List[t.Any] = (
186
188
  list(current_target) if isinstance(current_target, tuple) else current_target
187
189
  )
188
190
  # Mutates in-place by design for list targets to preserve merge performance.
@@ -260,16 +262,18 @@ class Utils:
260
262
  continue
261
263
 
262
264
  if Utils.is_overflow(current_source):
263
- source_of = t.cast(OverflowDict, current_source)
264
- sorted_pairs = sorted(_numeric_key_pairs(source_of), key=lambda item: item[0])
265
- numeric_keys = {key for _, key in sorted_pairs}
265
+ source_of: OverflowDict = t.cast(OverflowDict, current_source)
266
+ sorted_pairs: t.List[t.Tuple[int, t.Any]] = sorted(
267
+ _numeric_key_pairs(source_of), key=lambda item: item[0]
268
+ )
269
+ numeric_keys: t.Set[str] = {key for _, key in sorted_pairs}
266
270
  result = OverflowDict()
267
- offset = 0
271
+ offset: int = 0
268
272
  if not isinstance(current_target, Undefined):
269
273
  result["0"] = current_target
270
274
  offset = 1
271
275
  for numeric_key, key in sorted_pairs:
272
- val = source_of[key]
276
+ val: t.Any = source_of[key]
273
277
  if not isinstance(val, Undefined):
274
278
  # Offset ensures target occupies index "0"; source indices shift up by 1.
275
279
  result[str(numeric_key + offset)] = val
@@ -283,19 +287,17 @@ class Utils:
283
287
  continue
284
288
 
285
289
  result_list: t.List[t.Any] = []
286
- for element in (current_target,):
287
- if not isinstance(element, Undefined):
288
- result_list.append(element)
289
- for element in (current_source,):
290
- if not isinstance(element, Undefined):
291
- result_list.append(element)
290
+ if not isinstance(current_target, Undefined):
291
+ result_list.append(current_target)
292
+ if not isinstance(current_source, Undefined):
293
+ result_list.append(current_source)
292
294
  stack.pop()
293
295
  last_result = result_list
294
296
  continue
295
297
 
296
298
  # Prepare a mutable target we can merge into; reuse dict targets for performance.
297
299
  frame.merge_target = current_target if isinstance(current_target, dict) else dict(current_target)
298
- frame.merge_existing_keys = set(frame.merge_target.keys())
300
+ frame.merge_existing_keys = set(frame.merge_target)
299
301
  frame.pending_updates = {}
300
302
  frame.source_items = list(current_source.items())
301
303
  frame.entry_index = 0
@@ -304,7 +306,7 @@ class Utils:
304
306
  continue
305
307
 
306
308
  if frame.phase == "map_iter":
307
- merge_target = frame.merge_target
309
+ merge_target: t.Optional[t.MutableMapping[t.Any, t.Any]] = frame.merge_target
308
310
  if merge_target is None: # pragma: no cover - internal invariant
309
311
  raise RuntimeError("merge target is not initialized") # noqa: TRY003
310
312
 
@@ -441,7 +443,7 @@ class Utils:
441
443
  """
442
444
  i: int = len(value) - 1
443
445
  while i >= 0:
444
- item = value[i]
446
+ item: t.Any = value[i]
445
447
  if isinstance(item, Undefined):
446
448
  value.pop(i)
447
449
  elif isinstance(item, dict):
@@ -462,9 +464,9 @@ class Utils:
462
464
  `_dicts_are_equal` to avoid descending into the same mapping from itself.
463
465
  """
464
466
  # Snapshot keys so we can delete while iterating.
465
- keys: t.List[t.Any] = list(obj.keys())
467
+ keys: t.List[t.Any] = list(obj)
466
468
  for key in keys:
467
- val = obj[key]
469
+ val: t.Any = obj[key]
468
470
  if isinstance(val, Undefined):
469
471
  obj.pop(key)
470
472
  elif isinstance(val, dict) and not Utils._dicts_are_equal(val, obj):
@@ -514,8 +516,7 @@ class Utils:
514
516
  if not Utils._dicts_are_equal(v, d2[k], path):
515
517
  return False
516
518
  return True
517
- else:
518
- return d1 == d2
519
+ return d1 == d2
519
520
 
520
521
  @staticmethod
521
522
  def is_overflow(obj: t.Any) -> bool:
@@ -554,11 +555,11 @@ class Utils:
554
555
  """
555
556
  if Utils.is_overflow(a):
556
557
  # a is already an OverflowDict. Append b as one value at the next numeric index.
557
- orig_a = t.cast(OverflowDict, a)
558
- a_copy = orig_a.__class__({k: v for k, v in orig_a.items() if not isinstance(v, Undefined)})
558
+ orig_a: OverflowDict = t.cast(OverflowDict, a)
559
+ a_copy: OverflowDict = orig_a.__class__({k: v for k, v in orig_a.items() if not isinstance(v, Undefined)})
559
560
  # Use max key + 1 to handle sparse dicts safely, rather than len(a)
560
- key_pairs = _numeric_key_pairs(a_copy)
561
- idx = (max(key for key, _ in key_pairs) + 1) if key_pairs else 0
561
+ key_pairs: t.List[t.Tuple[int, str]] = _numeric_key_pairs(a_copy)
562
+ idx: int = (max(key for key, _ in key_pairs) + 1) if key_pairs else 0
562
563
 
563
564
  if not isinstance(b, Undefined):
564
565
  a_copy[str(idx)] = _copy_overflow_append_value(b)
@@ -567,17 +568,17 @@ class Utils:
567
568
  # Normal combination: flatten lists/tuples
568
569
  # Flatten a
569
570
  if isinstance(a, (list, tuple)):
570
- list_a = [x for x in a if not isinstance(x, Undefined)]
571
+ list_a: t.List[t.Any] = [x for x in a if not isinstance(x, Undefined)]
571
572
  else:
572
573
  list_a = [a] if not isinstance(a, Undefined) else []
573
574
 
574
575
  # Flatten b, handling OverflowDict as a list source
575
576
  if isinstance(b, (list, tuple)):
576
- list_b = [x for x in b if not isinstance(x, Undefined)]
577
+ list_b: t.List[t.Any] = [x for x in b if not isinstance(x, Undefined)]
577
578
  elif isinstance(b, CommaOverflowDict):
578
579
  list_b = [b]
579
580
  elif Utils.is_overflow(b):
580
- b_of = t.cast(OverflowDict, b)
581
+ b_of: OverflowDict = t.cast(OverflowDict, b)
581
582
  list_b = [
582
583
  b_of[k]
583
584
  for _, k in sorted(_numeric_key_pairs(b_of), key=lambda item: item[0])
@@ -586,9 +587,9 @@ class Utils:
586
587
  else:
587
588
  list_b = [b] if not isinstance(b, Undefined) else []
588
589
 
589
- res = [*list_a, *list_b]
590
+ res: t.List[t.Any] = [*list_a, *list_b]
590
591
 
591
- list_limit = options.list_limit if options else DecodeOptions().list_limit
592
+ list_limit: int = options.list_limit if options else DecodeOptions().list_limit
592
593
  if list_limit < 0:
593
594
  return OverflowDict({str(i): x for i, x in enumerate(res)}) if res else res
594
595
  if len(res) > list_limit:
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