From b5b6b9b845d94b747d5f13b54139b03d9ff665d7 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 9 Oct 2026 11:15:28 +0100 Subject: [PATCH 01/18] chore: enhance type hints for better clarity and compatibility --- src/qs_codec/encode.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/qs_codec/encode.py b/src/qs_codec/encode.py index 496e6e2..204973d 100644 --- a/src/qs_codec/encode.py +++ b/src/qs_codec/encode.py @@ -14,6 +14,8 @@ Nothing in this module mutates caller objects: inputs are shallow-normalized and deep-copied only where safe/necessary to honor options. """ +from __future__ import annotations + import sys import typing as t from collections.abc import Mapping as ABCMapping @@ -66,7 +68,7 @@ def encode(value: t.Any, options: t.Optional[EncodeOptions] = None) -> str: opts: EncodeOptions = options if options is not None else EncodeOptions() - filter_opt: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]] = opts.filter + filter_opt: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]] = opts.filter filter_is_callable: bool = callable(filter_opt) filter_is_sequence: bool = ( filter_opt is not None @@ -122,7 +124,7 @@ def encode(value: t.Any, options: t.Optional[EncodeOptions] = None) -> str: obj_keys = sorted(obj_keys, key=cmp_to_key(sort_opt)) # Side channel seed for legacy `_encode` compatibility (and cycle-state bootstrap when provided). - side_channel: WeakKeyDictionary = WeakKeyDictionary() + side_channel: WeakKeyDictionary[t.Any, t.Any] = WeakKeyDictionary() max_depth: int = _get_max_encode_depth(opts.max_depth) # Encode each selected root key. @@ -223,7 +225,9 @@ def _identity_key(value: t.Any) -> int: return id(value) -def _bootstrap_cycle_state_from_side_channel(side_channel: WeakKeyDictionary) -> t.Tuple[CycleState, int]: +def _bootstrap_cycle_state_from_side_channel( + side_channel: WeakKeyDictionary[t.Any, t.Any], +) -> t.Tuple[CycleState, int]: """ Build O(1) ancestry lookup state from an existing side-channel chain. @@ -231,7 +235,7 @@ def _bootstrap_cycle_state_from_side_channel(side_channel: WeakKeyDictionary) -> Tuple of (state, current_level), where current_level is the chain length from the current frame to the top-most side-channel mapping. """ - chain: t.List[WeakKeyDictionary] = [] + chain: t.List[WeakKeyDictionary[t.Any, t.Any]] = [] tmp_sc: t.Any = side_channel.get(_sentinel) while isinstance(tmp_sc, WeakKeyDictionary): chain.append(tmp_sc) @@ -292,18 +296,18 @@ def _pop_current_node(state: CycleState, node_key: t.Any) -> None: _COMMA_GENERATOR: t.Callable[[str, t.Optional[str]], str] = ListFormat.COMMA.generator -def _uses_legacy_cycle_state(side_channel: WeakKeyDictionary) -> bool: +def _uses_legacy_cycle_state(side_channel: WeakKeyDictionary[t.Any, t.Any]) -> bool: return isinstance(side_channel.get(_sentinel), WeakKeyDictionary) def _linear_chain_fast_path_eligible( is_undefined: bool, - side_channel: WeakKeyDictionary, + side_channel: WeakKeyDictionary[t.Any, t.Any], comma_round_trip: t.Optional[bool], comma_compact_nulls: bool, encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]], sort: t.Optional[t.Callable[[t.Any, t.Any], int]], - filter_: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]], + filter_: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]], generate_array_prefix: t.Callable[[str, t.Optional[str]], str], allow_empty_lists: bool, strict_null_handling: bool, @@ -336,14 +340,14 @@ def _linear_chain_fast_path_eligible( def _try_encode_linear_chain_plain_dict( value: t.Any, is_undefined: bool, - side_channel: WeakKeyDictionary, + side_channel: WeakKeyDictionary[t.Any, t.Any], prefix: t.Optional[str], comma_round_trip: t.Optional[bool], comma_compact_nulls: bool, encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]], serialize_date: t.Union[t.Callable[[datetime], t.Optional[str]], str], sort: t.Optional[t.Callable[[t.Any, t.Any], int]], - filter_: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]], + filter_: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]], formatter: t.Optional[t.Callable[[str], str]], format: Format, generate_array_prefix: t.Callable[[str, t.Optional[str]], str], @@ -447,14 +451,14 @@ def _try_encode_linear_chain_plain_dict( def _try_encode_linear_chain( value: t.Any, is_undefined: bool, - side_channel: WeakKeyDictionary, + side_channel: WeakKeyDictionary[t.Any, t.Any], prefix: t.Optional[str], comma_round_trip: t.Optional[bool], comma_compact_nulls: bool, encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]], serialize_date: t.Union[t.Callable[[datetime], t.Optional[str]], str], sort: t.Optional[t.Callable[[t.Any, t.Any], int]], - filter_: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]], + filter_: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]], formatter: t.Optional[t.Callable[[str], str]], format: Format, generate_array_prefix: t.Callable[[str, t.Optional[str]], str], @@ -591,14 +595,14 @@ def _next_path_for_sequence( def _encode( value: t.Any, is_undefined: bool, - side_channel: WeakKeyDictionary, + side_channel: WeakKeyDictionary[t.Any, t.Any], prefix: t.Optional[str], comma_round_trip: t.Optional[bool], comma_compact_nulls: bool, encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]], serialize_date: t.Union[t.Callable[[datetime], t.Optional[str]], str], sort: t.Optional[t.Callable[[t.Any, t.Any], int]], - filter_: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]], + filter_: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]], formatter: t.Optional[t.Callable[[str], str]], format: Format = Format.RFC3986, generate_array_prefix: t.Callable[[str, t.Optional[str]], str] = ListFormat.INDICES.generator, @@ -800,7 +804,9 @@ def _append_child_result(frame: EncodeFrame, encoded: t.Any) -> None: frame.formatter = frame.format.formatter obj: t.Any = frame.value - filter_opt: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]] = frame.filter_ + filter_opt: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]] = ( + frame.filter_ + ) filter_is_sequence: bool = ( filter_opt is not None and isinstance(filter_opt, ABCSequence) From 1c6c9b4cf66adbb0650427751cae53318273a2a6 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 9 Oct 2026 11:15:32 +0100 Subject: [PATCH 02/18] chore: refine type hints for side_channel and filter_ attributes --- src/qs_codec/models/encode_frame.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/qs_codec/models/encode_frame.py b/src/qs_codec/models/encode_frame.py index 7827a71..c89241e 100644 --- a/src/qs_codec/models/encode_frame.py +++ b/src/qs_codec/models/encode_frame.py @@ -1,5 +1,7 @@ """Mutable traversal frame for iterative encoding.""" +from __future__ import annotations + import typing as t from datetime import datetime from weakref import WeakKeyDictionary @@ -55,14 +57,14 @@ class EncodeFrame: ) value: t.Any is_undefined: bool - side_channel: WeakKeyDictionary + side_channel: WeakKeyDictionary[t.Any, t.Any] prefix: t.Optional[str] comma_round_trip: t.Optional[bool] comma_compact_nulls: bool encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]] serialize_date: t.Union[t.Callable[[datetime], t.Optional[str]], str] sort: t.Optional[t.Callable[[t.Any, t.Any], int]] - filter_: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]] + filter_: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]] formatter: t.Optional[t.Callable[[str], str]] format: Format generate_array_prefix: t.Callable[[str, t.Optional[str]], str] @@ -95,14 +97,14 @@ def __init__( self, value: t.Any, is_undefined: bool, - side_channel: WeakKeyDictionary, + side_channel: WeakKeyDictionary[t.Any, t.Any], prefix: t.Optional[str], comma_round_trip: t.Optional[bool], comma_compact_nulls: bool, encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]], serialize_date: t.Union[t.Callable[[datetime], t.Optional[str]], str], sort: t.Optional[t.Callable[[t.Any, t.Any], int]], - filter_: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]], + filter_: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]], formatter: t.Optional[t.Callable[[str], str]], format: Format, generate_array_prefix: t.Callable[[str, t.Optional[str]], str], From 720f752a7f94e170d39eb6be47f602b83cfe2feb Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 9 Oct 2026 11:15:38 +0100 Subject: [PATCH 03/18] chore: refine type hint for filter attribute to enhance type specificity --- src/qs_codec/models/encode_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qs_codec/models/encode_options.py b/src/qs_codec/models/encode_options.py index 3747e88..b2d3eca 100644 --- a/src/qs_codec/models/encode_options.py +++ b/src/qs_codec/models/encode_options.py @@ -67,7 +67,7 @@ class EncodeOptions: """Space handling and percent-encoding style. `RFC3986` encodes spaces as `%20`, while `RFC1738` uses `+`.""" - filter: t.Optional[t.Union[t.Callable, t.Sequence[t.Union[str, int]]]] = field(default=None) + filter: t.Optional[t.Union[t.Callable[[str, t.Any], t.Any], t.Sequence[t.Union[str, int]]]] = field(default=None) """Restrict which keys get included. - If a callable is provided, it is invoked for each key and should return the replacement value (or `None` to drop when `skip_nulls` applies). From b6fb854063f5a29f03d2619ad33237adb5d0dc13 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 9 Oct 2026 11:15:42 +0100 Subject: [PATCH 04/18] chore: refine OverflowDict class to use specific type hints for better type safety --- src/qs_codec/models/overflow_dict.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/qs_codec/models/overflow_dict.py b/src/qs_codec/models/overflow_dict.py index 46f2897..26c7172 100644 --- a/src/qs_codec/models/overflow_dict.py +++ b/src/qs_codec/models/overflow_dict.py @@ -3,9 +3,16 @@ from __future__ import annotations import copy +import typing as t +# Subscripted at type-check time only; subclassing `t.Dict[...]` would add `Generic` to the MRO on 3.8. +if t.TYPE_CHECKING: + _DictBase = t.Dict[t.Any, t.Any] +else: + _DictBase = dict -class OverflowDict(dict): + +class OverflowDict(_DictBase): """A mutable marker for list overflows when `list_limit` is exceeded.""" def copy(self) -> "OverflowDict": @@ -16,7 +23,7 @@ def __copy__(self) -> "OverflowDict": """Return an OverflowDict copy to preserve the overflow marker.""" return self.__class__(super().copy()) - def __deepcopy__(self, memo: dict[int, object]) -> "OverflowDict": + def __deepcopy__(self, memo: t.Dict[int, t.Any]) -> "OverflowDict": """Return an OverflowDict deepcopy to preserve the overflow marker.""" copied = self.__class__() memo[id(self)] = copied From a39475e0884657ab816bd430b7d63a5c988d3272 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 9 Oct 2026 11:15:46 +0100 Subject: [PATCH 05/18] chore: add type hints to __new__, __copy__, __deepcopy__, __reduce__, and __init_subclass__ methods for improved type safety --- src/qs_codec/models/undefined.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/qs_codec/models/undefined.py b/src/qs_codec/models/undefined.py index 4bbfa27..efca52e 100644 --- a/src/qs_codec/models/undefined.py +++ b/src/qs_codec/models/undefined.py @@ -26,7 +26,7 @@ class Undefined: _lock: t.ClassVar[threading.Lock] = threading.Lock() _instance: t.ClassVar[t.Optional["Undefined"]] = None - def __new__(cls): + def __new__(cls) -> "Undefined": """Return the singleton instance. Repeated construction returns the same object reference so identity @@ -42,19 +42,19 @@ def __repr__(self) -> str: # pragma: no cover - trivial """Return a string representation of the singleton.""" return "Undefined()" - def __copy__(self): # pragma: no cover - trivial + def __copy__(self) -> "Undefined": # pragma: no cover - trivial """Preserve singleton identity when shallow-copied.""" return self - def __deepcopy__(self, memo): # pragma: no cover - trivial + def __deepcopy__(self, memo: t.Dict[int, t.Any]) -> "Undefined": # pragma: no cover - trivial """Preserve singleton identity when deep-copied.""" return self - def __reduce__(self): # pragma: no cover - trivial + def __reduce__(self) -> t.Tuple[t.Type["Undefined"], t.Tuple[()]]: # pragma: no cover - trivial """Reconstruct through the singleton constructor when unpickled.""" return Undefined, () - def __init_subclass__(cls, **kwargs): # pragma: no cover - defensive + def __init_subclass__(cls, **kwargs: t.Any) -> None: # pragma: no cover - defensive """Prevent subclassing to preserve singleton identity.""" raise TypeError("Undefined cannot be subclassed") From bac486292de44713461c97b02fd4236c0d267eea Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 9 Oct 2026 11:15:51 +0100 Subject: [PATCH 06/18] chore: refine type hints for stack and node variables in Utils class for improved type safety --- src/qs_codec/utils/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qs_codec/utils/utils.py b/src/qs_codec/utils/utils.py index 50015c5..bab8451 100644 --- a/src/qs_codec/utils/utils.py +++ b/src/qs_codec/utils/utils.py @@ -402,12 +402,12 @@ def compact(root: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: The same `root` object for chaining. """ # Depth-first traversal without recursion. - stack: t.Deque[t.Union[t.Dict, t.List]] = deque([root]) + stack: t.Deque[t.Union[t.Dict[t.Any, t.Any], t.List[t.Any]]] = deque([root]) # Track object identities to avoid revisiting in cycles. visited: t.Set[int] = {id(root)} while stack: - node: t.Union[t.Dict, t.List] = stack.pop() + node: t.Union[t.Dict[t.Any, t.Any], t.List[t.Any]] = stack.pop() if isinstance(node, dict): # Copy keys so we can delete from the dict during iteration. for k in list(node.keys()): @@ -599,7 +599,7 @@ def combine( @staticmethod def apply( val: t.Union[t.List[t.Any], t.Tuple[t.Any], t.Any], - fn: t.Callable, + fn: t.Callable[[t.Any], t.Any], ) -> t.Union[t.List[t.Any], t.Any]: """ Map a callable over a value or sequence. From ea782fc35bd2af769db4656abe9a1c7107f87438 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 12:36:35 +0100 Subject: [PATCH 07/18] chore: refine type hints for variables in decode functions for improved type safety --- src/qs_codec/decode.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/qs_codec/decode.py b/src/qs_codec/decode.py index 7dee88f..10366ca 100644 --- a/src/qs_codec/decode.py +++ b/src/qs_codec/decode.py @@ -168,12 +168,12 @@ def _leading_structured_root(key: str, options: DecodeOptions) -> str: if not segments: return key - first = segments[0] + first: str = segments[0] if not first.startswith("["): return first - last = first.rfind("]") - clean_root = first[1:last] if last > 0 else first[1:] + last: int = first.rfind("]") + clean_root: str = first[1:last] if last > 0 else first[1:] return clean_root or "0" @@ -243,8 +243,8 @@ def _parse_array_value( """ if isinstance(value, str) and value and options.comma and "," in value: if options.raise_on_limit_exceeded: - comma_count = 0 - comma_index = value.find(",") + comma_count: int = 0 + comma_index: int = value.find(",") while comma_index >= 0: comma_count += 1 if comma_count >= options.list_limit: @@ -361,7 +361,7 @@ def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str raw_key: str = "" list_limit_exceeded: bool = False if pos == -1: - key_decoded = decoder_fn(part, charset, kind=DecodeKind.KEY) + key_decoded: t.Optional[str] = decoder_fn(part, charset, kind=DecodeKind.KEY) if key_decoded is None: continue key: str = key_decoded @@ -376,7 +376,7 @@ def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str key = key_decoded if not key: continue - parsed_value = _parse_array_value( + parsed_value: t.Any = _parse_array_value( part[pos + 1 :], options, len(obj[key]) if key in obj and isinstance(obj[key], (list, tuple)) else 0, @@ -404,7 +404,7 @@ def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str val = CommaOverflowDict({str(i): item for i, item in enumerate(val)}) existing: bool = key in obj - part_duplicates = Duplicates.COMBINE if bracket_array_assignment else duplicates + part_duplicates: Duplicates = Duplicates.COMBINE if bracket_array_assignment else duplicates # Combine/overwrite according to the configured duplicates policy. if existing and part_duplicates == Duplicates.COMBINE: From ee61e27cd9fef18a2e63bf2b6bba77eec0fe3e7d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 12:39:49 +0100 Subject: [PATCH 08/18] chore: refine type hints for variables in _encode function for improved type safety --- src/qs_codec/encode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qs_codec/encode.py b/src/qs_codec/encode.py index 204973d..db14d59 100644 --- a/src/qs_codec/encode.py +++ b/src/qs_codec/encode.py @@ -708,7 +708,7 @@ def _encode( return fast_path_result last_result: t.Union[t.List[t.Any], t.Tuple[t.Any, ...], t.Any, None] = None - use_legacy_cycle_state = _uses_legacy_cycle_state(side_channel) + use_legacy_cycle_state: bool = _uses_legacy_cycle_state(side_channel) public_cycle_state = CycleState() last_bracket_key: t.Optional[str] = None last_bracket_segment: t.Optional[str] = None @@ -730,7 +730,7 @@ def _dot_segment(encoded_key: str) -> str: if last_dot_key == encoded_key and last_dot_segment is not None: return last_dot_segment - segment = f".{encoded_key}" + segment: str = f".{encoded_key}" last_dot_key = encoded_key last_dot_segment = segment return segment From e018111eb2d05e4e44625c26ca75e7d745a7ed25 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 12:53:03 +0100 Subject: [PATCH 09/18] chore: refine type hints for variables in DecodeUtils class for improved type safety --- src/qs_codec/utils/decode_utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/qs_codec/utils/decode_utils.py b/src/qs_codec/utils/decode_utils.py index e96a734..d636ee3 100644 --- a/src/qs_codec/utils/decode_utils.py +++ b/src/qs_codec/utils/decode_utils.py @@ -246,16 +246,16 @@ def split_key_into_segments( open_idx: int = first depth: int = 0 - unterminated = False + unterminated: bool = False while open_idx >= 0 and depth < max_depth: - level = 1 - i = open_idx + 1 - close = -1 + level: int = 1 + i: int = open_idx + 1 + close: int = -1 # Balance nested '[' and ']' inside the same group, # so "[withbracket[]]" is treated as *one* segment. while i < n: - ch = key[i] + ch: str = key[i] if ch == "[": level += 1 elif ch == "]": @@ -277,7 +277,7 @@ def split_key_into_segments( if open_idx >= 0: # We only want to raise for true depth overflow under strict_depth, # not for unterminated bracket groups. - depth_overflow = (depth >= max_depth) and not unterminated + depth_overflow: bool = (depth >= max_depth) and not unterminated if strict_depth and depth_overflow: raise IndexError(f"Input depth exceeded depth option of {max_depth} and strict_depth is True") # Stash the remainder as a single segment (qs parity) From 5a42778099cc0618f25da06016a8346a43291a12 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 12:57:23 +0100 Subject: [PATCH 10/18] chore: refine type hints for variables in EncodeUtils class for improved type safety --- src/qs_codec/utils/encode_utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/qs_codec/utils/encode_utils.py b/src/qs_codec/utils/encode_utils.py index 10a5184..368d007 100644 --- a/src/qs_codec/utils/encode_utils.py +++ b/src/qs_codec/utils/encode_utils.py @@ -147,7 +147,9 @@ def _encode_string(cls, string: str, format: t.Optional[Format]) -> str: *code units*, matching JavaScript semantics. We then walk the string with a manual index, skipping the low surrogate when we emit a surrogate pair. """ - safe_chars_ascii = cls.RFC1738_SAFE_CHARS_ASCII if format == Format.RFC1738 else cls.SAFE_CHARS_ASCII + safe_chars_ascii: t.Tuple[bool, ...] = ( + cls.RFC1738_SAFE_CHARS_ASCII if format == Format.RFC1738 else cls.SAFE_CHARS_ASCII + ) if string.isascii(): if cls._all_ascii_safe(string, safe_chars_ascii): return string @@ -158,10 +160,10 @@ def _encode_string(cls, string: str, format: t.Optional[Format]) -> str: hex_table: t.Tuple[str, ...] = cls.HEX_TABLE buffer: t.List[str] = [] - i = 0 - n = len(s) + i: int = 0 + n: int = len(s) while i < n: - c = ord(s[i]) + c: int = ord(s[i]) if c < 0x80 and safe_chars_ascii[c]: buffer.append(s[i]) @@ -204,7 +206,9 @@ def _encode_string(cls, string: str, format: t.Optional[Format]) -> str: @classmethod def _is_safe_char(cls, c: int, format: t.Optional[Format]) -> bool: """Return True if code unit `c` is allowed unescaped for the given `format`.""" - safe_chars_ascii = cls.RFC1738_SAFE_CHARS_ASCII if format == Format.RFC1738 else cls.SAFE_CHARS_ASCII + safe_chars_ascii: t.Tuple[bool, ...] = ( + cls.RFC1738_SAFE_CHARS_ASCII if format == Format.RFC1738 else cls.SAFE_CHARS_ASCII + ) return c < 0x80 and safe_chars_ascii[c] @classmethod @@ -233,7 +237,7 @@ def _encode_char(cls, string: str, i: int, c: int) -> t.List[str]: @classmethod def _encode_surrogate_pair(cls, string: str, i: int, c: int) -> t.List[str]: """Encode a surrogate pair starting at `i` as a 4-byte UTF-8 sequence.""" - low = ord(string[i + 1]) + low: int = ord(string[i + 1]) c = 0x10000 + (((c & 0x3FF) << 10) | (low & 0x3FF)) return [ cls.HEX_TABLE[0xF0 | (c >> 18)], From a5e80685d17ea9a21993b938d12b05464374a821 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 18:35:54 +0100 Subject: [PATCH 11/18] chore: refine type hints for decoder-related variables in DecodeOptions class for improved type safety --- src/qs_codec/models/decode_options.py | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/qs_codec/models/decode_options.py b/src/qs_codec/models/decode_options.py index 7358834..6002c6a 100644 --- a/src/qs_codec/models/decode_options.py +++ b/src/qs_codec/models/decode_options.py @@ -171,13 +171,13 @@ def __post_init__(self) -> None: # decoder setup + compatibility wrapper: # precedence is: user `decoder` > `legacy_decoder` > library default. - raw_dec = self.decoder + raw_dec: t.Optional[t.Callable[..., t.Optional[t.Any]]] = self.decoder if raw_dec is None and self.legacy_decoder is not None: raw_dec = self.legacy_decoder # legacy two-arg form; no kind if raw_dec is None: raw_dec = DecodeUtils.decode - default_decoder = DecodeUtils.decode + default_decoder: t.Callable[..., t.Optional[str]] = DecodeUtils.decode # Fast path for the library default decoder: skip adapter dispatch completely. if raw_dec is default_decoder or getattr(raw_dec, "__func__", None) is getattr( default_decoder, "__func__", None @@ -185,19 +185,19 @@ def __post_init__(self) -> None: self.decoder = DecodeUtils.decode return - user_dec = raw_dec + user_dec: t.Callable[..., t.Optional[t.Any]] = raw_dec # Precompute dispatch to avoid per-call introspection. try: - sig = inspect.signature(user_dec) - params = sig.parameters - param_list = list(params.values()) + sig: inspect.Signature = inspect.signature(user_dec) + params: t.Mapping[str, inspect.Parameter] = sig.parameters + param_list: t.List[inspect.Parameter] = list(params.values()) - has_var_kw = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in param_list) - has_var_pos = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in param_list) + has_var_kw: bool = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in param_list) + has_var_pos: bool = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in param_list) - accepts_charset_pos = False - accepts_charset_kw = False + accepts_charset_pos: bool = False + accepts_charset_kw: bool = False if "charset" in params: p = params["charset"] if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD): @@ -207,9 +207,9 @@ def __post_init__(self) -> None: if has_var_pos: accepts_charset_pos = True - has_kind_param = "kind" in params - accepts_kind_kw = False - accepts_kind_pos = False + has_kind_param: bool = "kind" in params + accepts_kind_kw: bool = False + accepts_kind_pos: bool = False if has_kind_param: k = params["kind"] accepts_kind_kw = k.kind in ( @@ -225,9 +225,9 @@ def __post_init__(self) -> None: accepts_kind_pos = False # Decide how to represent `kind`: prefer string for maximum compatibility - pass_kind_as_str = True + pass_kind_as_str: bool = True if has_kind_param: - ann = params["kind"].annotation + ann: t.Any = params["kind"].annotation # NOTE: If a user annotates `kind` as a typing.Literal (e.g., Literal["key", "value"]), # `ann` will NOT be a `type`, so we fall back to passing strings (the default path below). # This is intentional: it preserves compatibility with callables that prefer plain strings @@ -299,7 +299,7 @@ def decode( values; whether a ``.`` participates in key splitting is decided later by the parser. """ # ``self.decoder`` has been normalized to accept (s, charset, *, kind) - d = self.decoder + d: t.Optional[t.Callable[..., t.Optional[t.Any]]] = self.decoder if d is None: # Should not happen because we always set an adapter, but keep a safe fallback. return DecodeUtils.decode(value, charset or self.charset) @@ -310,7 +310,7 @@ def decode_key(self, value: t.Optional[str], charset: t.Optional[Charset] = None Note: custom decoders returning non-strings for keys are coerced via ``str()``. """ - out = self.decode(value, charset, kind=DecodeKind.KEY) + out: t.Optional[t.Any] = self.decode(value, charset, kind=DecodeKind.KEY) return None if out is None else str(out) def decode_value(self, value: t.Optional[str], charset: t.Optional[Charset] = None) -> t.Optional[t.Any]: From 515805f76c5315a491ceb34ba734dab76f55639f Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 18:37:12 +0100 Subject: [PATCH 12/18] chore: refine type hints for variables in EncodeOptions class for improved type safety --- src/qs_codec/models/encode_options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qs_codec/models/encode_options.py b/src/qs_codec/models/encode_options.py index b2d3eca..7108c92 100644 --- a/src/qs_codec/models/encode_options.py +++ b/src/qs_codec/models/encode_options.py @@ -97,9 +97,9 @@ def encoder(self) -> t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]] The returned callable has signature `(value) -> str` and internally calls the underlying `_encoder(value, self.charset, self.format)`. """ - raw_encoder = self._encoder - charset = self.charset - format_ = self.format + raw_encoder: t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str] = self._encoder + charset: Charset = self.charset + format_: Format = self.format def bound_encoder( value: t.Any, From 2878b07e5993fdbca33676f438b528bdd8b38b7d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 18:39:27 +0100 Subject: [PATCH 13/18] chore: refine type hints for variables in KeyPathNode class for improved type safety --- src/qs_codec/models/key_path_node.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/qs_codec/models/key_path_node.py b/src/qs_codec/models/key_path_node.py index e88b6c6..ae330bf 100644 --- a/src/qs_codec/models/key_path_node.py +++ b/src/qs_codec/models/key_path_node.py @@ -42,7 +42,7 @@ def append(self, segment: str) -> "KeyPathNode": def as_dot_encoded(self) -> "KeyPathNode": """Return a cached view where literal dots are encoded as ``%2E``.""" - cached = self.dot_encoded + cached: t.Optional["KeyPathNode"] = self.dot_encoded if cached is not None: return cached @@ -54,11 +54,11 @@ def as_dot_encoded(self) -> "KeyPathNode": for current in reversed(unresolved): encoded_segment = current.segment.replace(".", "%2E") if "." in current.segment else current.segment - parent = current.parent + parent: t.Optional[KeyPathNode] = current.parent if parent is None: encoded = current if encoded_segment is current.segment else KeyPathNode(None, encoded_segment) else: - encoded_parent = parent.dot_encoded + encoded_parent: t.Optional[KeyPathNode] = parent.dot_encoded if encoded_parent is None: # pragma: no cover - internal invariant raise RuntimeError("dot_encoded parent is not initialized") # noqa: TRY003 if encoded_parent is parent and encoded_segment is current.segment: @@ -71,40 +71,40 @@ def as_dot_encoded(self) -> "KeyPathNode": def materialize(self) -> str: """Render and cache the full path text.""" - cached = self.materialized + cached: t.Optional[str] = self.materialized if cached is not None: return cached - parent = self.parent + parent: t.Optional["KeyPathNode"] = self.parent if parent is None: self.materialized = self.segment return self.segment if self.depth == 2: - parent_part = parent.materialized + parent_part: t.Optional[str] = parent.materialized if parent_part is None: parent_part = parent.segment parent.materialized = parent_part - value = parent_part + self.segment + value: str = parent_part + self.segment self.materialized = value return value suffix_nodes: t.List["KeyPathNode"] = [] current: t.Optional["KeyPathNode"] = self - base = "" + base: str = "" while current is not None: - cached_current = current.materialized + cached_current: t.Optional[str] = current.materialized if cached_current is not None: base = cached_current break suffix_nodes.append(current) current = current.parent - suffix_count = len(suffix_nodes) + suffix_count: int = len(suffix_nodes) prefix_lengths: t.List[int] = [0] * suffix_count parts: t.List[str] = [base] if base else [] - prefix_length = len(base) - forward_index = 0 + prefix_length: int = len(base) + forward_index: int = 0 for index in range(suffix_count - 1, -1, -1): node = suffix_nodes[index] parts.append(node.segment) @@ -116,13 +116,13 @@ def materialize(self) -> str: self.materialized = value def cache_forward_index(index: int) -> None: - node = suffix_nodes[suffix_count - 1 - index] + node: KeyPathNode = suffix_nodes[suffix_count - 1 - index] if node.materialized is None: node.materialized = value[: prefix_lengths[index]] # `self.materialized` is already populated above, so count the leaf # toward the cache budget without performing a redundant write. - cached_nodes = 1 + cached_nodes: int = 1 if suffix_count > 1 and cached_nodes < self._MATERIALIZE_CACHE_NODE_LIMIT: cache_forward_index(0) From f83d030bf00d715e353dcbb1ba50536604db225b Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 18:40:59 +0100 Subject: [PATCH 14/18] chore: refine type hints for __slots__ in Undefined class for improved type safety --- src/qs_codec/models/undefined.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qs_codec/models/undefined.py b/src/qs_codec/models/undefined.py index efca52e..b577b19 100644 --- a/src/qs_codec/models/undefined.py +++ b/src/qs_codec/models/undefined.py @@ -22,7 +22,7 @@ class Undefined: returning public results. """ - __slots__ = () + __slots__: t.Tuple[str, ...] = () _lock: t.ClassVar[threading.Lock] = threading.Lock() _instance: t.ClassVar[t.Optional["Undefined"]] = None From d42acfa60ac8be5b4bd92860628f6a9a08cec7b8 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 18:42:01 +0100 Subject: [PATCH 15/18] chore: refine type hints for __slots__ in _Proxy and WeakWrapper classes for improved type safety --- src/qs_codec/models/weak_wrapper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qs_codec/models/weak_wrapper.py b/src/qs_codec/models/weak_wrapper.py index 3c13016..7b5cd3a 100644 --- a/src/qs_codec/models/weak_wrapper.py +++ b/src/qs_codec/models/weak_wrapper.py @@ -14,14 +14,14 @@ class _Proxy: NOTE: Proxies must be weak-referenceable because the cache holds them in a WeakValueDictionary. That requires "__weakref__" in __slots__. """ - __slots__ = ("value", "__weakref__") + __slots__: t.Tuple[str, ...] = ("value", "__weakref__") def __init__(self, value: t.Any) -> None: """Strong ref to the value so hash/equality can access it while a wrapper keeps this proxy alive.""" self.value = value -__all__ = ["WeakWrapper", "_proxy_cache"] +__all__: t.Tuple[str, ...] = ("WeakWrapper", "_proxy_cache") # Exported for tests @@ -48,7 +48,7 @@ class WeakWrapper: - Equality is proxy identity; hash is the proxy identity (stable across mutations). """ - __slots__ = ("_proxy", "_wref", "__weakref__") + __slots__: t.Tuple[str, ...] = ("_proxy", "_wref", "__weakref__") _proxy: _Proxy _wref: ReferenceType[_Proxy] From 610bc27806aee2a800734f1afe1bfa78e538a35e Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 18:44:28 +0100 Subject: [PATCH 16/18] chore: refine type hints for __all__ in DecodeKind class for improved type safety --- src/qs_codec/enums/decode_kind.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qs_codec/enums/decode_kind.py b/src/qs_codec/enums/decode_kind.py index 2e60a12..6705500 100644 --- a/src/qs_codec/enums/decode_kind.py +++ b/src/qs_codec/enums/decode_kind.py @@ -7,6 +7,7 @@ options (`allow_dots`, `decode_dot_in_keys`). """ +import typing as t from enum import Enum @@ -28,4 +29,4 @@ class DecodeKind(str, Enum): VALUE = "value" # Value decode; fully percent-decode -__all__ = ["DecodeKind"] +__all__: t.Tuple[str, ...] = ("DecodeKind",) From ed181e67b98b159c00377b8589080b5716047ac5 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 18:45:12 +0100 Subject: [PATCH 17/18] chore: refine type hints for __all__ in sentinel module for improved type safety --- src/qs_codec/enums/sentinel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qs_codec/enums/sentinel.py b/src/qs_codec/enums/sentinel.py index 963f0f2..83e9aab 100644 --- a/src/qs_codec/enums/sentinel.py +++ b/src/qs_codec/enums/sentinel.py @@ -7,10 +7,11 @@ fragment that appears on the wire. """ +import typing as t from dataclasses import dataclass from enum import Enum -__all__ = ("Sentinel",) +__all__: t.Tuple[str, ...] = ("Sentinel",) @dataclass(frozen=True) From d36581845e0b31aac96bd5cbc55730f845bfeaae Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 10 Oct 2026 18:57:14 +0100 Subject: [PATCH 18/18] chore: add pragma comment for type-checking in overflow_dict.py --- src/qs_codec/models/overflow_dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qs_codec/models/overflow_dict.py b/src/qs_codec/models/overflow_dict.py index 26c7172..4d0efb8 100644 --- a/src/qs_codec/models/overflow_dict.py +++ b/src/qs_codec/models/overflow_dict.py @@ -6,7 +6,7 @@ import typing as t # Subscripted at type-check time only; subclassing `t.Dict[...]` would add `Generic` to the MRO on 3.8. -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover - type-checking only _DictBase = t.Dict[t.Any, t.Any] else: _DictBase = dict