Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b5b6b9b
chore: enhance type hints for better clarity and compatibility
techouse Oct 9, 2026
1c6c9b4
chore: refine type hints for side_channel and filter_ attributes
techouse Oct 9, 2026
720f752
chore: refine type hint for filter attribute to enhance type specificity
techouse Oct 9, 2026
b6fb854
chore: refine OverflowDict class to use specific type hints for bette…
techouse Oct 9, 2026
a39475e
chore: add type hints to __new__, __copy__, __deepcopy__, __reduce__,…
techouse Oct 9, 2026
bac4862
chore: refine type hints for stack and node variables in Utils class …
techouse Oct 9, 2026
ea782fc
chore: refine type hints for variables in decode functions for improv…
techouse Oct 10, 2026
ee61e27
chore: refine type hints for variables in _encode function for improv…
techouse Oct 10, 2026
e018111
chore: refine type hints for variables in DecodeUtils class for impro…
techouse Oct 10, 2026
5a42778
chore: refine type hints for variables in EncodeUtils class for impro…
techouse Oct 10, 2026
a5e8068
chore: refine type hints for decoder-related variables in DecodeOptio…
techouse Oct 10, 2026
515805f
chore: refine type hints for variables in EncodeOptions class for imp…
techouse Oct 10, 2026
2878b07
chore: refine type hints for variables in KeyPathNode class for impro…
techouse Oct 10, 2026
f83d030
chore: refine type hints for __slots__ in Undefined class for improve…
techouse Oct 10, 2026
d42acfa
chore: refine type hints for __slots__ in _Proxy and WeakWrapper clas…
techouse Oct 10, 2026
610bc27
chore: refine type hints for __all__ in DecodeKind class for improved…
techouse Oct 10, 2026
ed181e6
chore: refine type hints for __all__ in sentinel module for improved …
techouse Oct 10, 2026
d365818
chore: add pragma comment for type-checking in overflow_dict.py
techouse Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/qs_codec/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
38 changes: 22 additions & 16 deletions src/qs_codec/encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -223,15 +225,17 @@ 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.

Returns:
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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -704,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
Expand All @@ -726,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
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/qs_codec/enums/decode_kind.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
options (`allow_dots`, `decode_dot_in_keys`).
"""

import typing as t
from enum import Enum


Expand All @@ -28,4 +29,4 @@ class DecodeKind(str, Enum):
VALUE = "value" # Value decode; fully percent-decode


__all__ = ["DecodeKind"]
__all__: t.Tuple[str, ...] = ("DecodeKind",)
3 changes: 2 additions & 1 deletion src/qs_codec/enums/sentinel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 17 additions & 17 deletions src/qs_codec/models/decode_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,33 +171,33 @@ 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
):
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):
Expand All @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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]:
Expand Down
10 changes: 6 additions & 4 deletions src/qs_codec/models/encode_frame.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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],
Expand Down
8 changes: 4 additions & 4 deletions src/qs_codec/models/encode_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading