4545from urllib .parse import quote , unquote
4646
4747__all__ = [
48- "DEFAULT_MAX_EXPRESSIONS" ,
4948 "DEFAULT_MAX_TEMPLATE_LENGTH" ,
49+ "DEFAULT_MAX_VARIABLES" ,
5050 "DEFAULT_MAX_URI_LENGTH" ,
5151 "InvalidUriTemplate" ,
5252 "Operator" ,
6363# (Percent-encoded varchars are technically allowed but unseen in practice.)
6464_VARNAME_RE = re .compile (r"^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*$" )
6565
66- DEFAULT_MAX_TEMPLATE_LENGTH = 1_000_000
67- DEFAULT_MAX_EXPRESSIONS = 10_000
66+ DEFAULT_MAX_TEMPLATE_LENGTH = 8_192
67+ DEFAULT_MAX_VARIABLES = 256
6868DEFAULT_MAX_URI_LENGTH = 65_536
6969
7070# RFC 3986 reserved characters, kept unencoded by {+var} and {#var}.
@@ -284,12 +284,12 @@ class UriTemplate:
284284 """
285285
286286 template : str
287- _parts : list [_Part ] = field (repr = False , compare = False )
288- _variables : list [Variable ] = field (repr = False , compare = False )
289- _prefix : list [_Atom ] = field (repr = False , compare = False )
287+ _parts : tuple [_Part , ... ] = field (repr = False , compare = False )
288+ _variables : tuple [Variable , ... ] = field (repr = False , compare = False )
289+ _prefix : tuple [_Atom , ... ] = field (repr = False , compare = False )
290290 _greedy : Variable | None = field (repr = False , compare = False )
291- _suffix : list [_Atom ] = field (repr = False , compare = False )
292- _query_variables : list [Variable ] = field (repr = False , compare = False )
291+ _suffix : tuple [_Atom , ... ] = field (repr = False , compare = False )
292+ _query_variables : tuple [Variable , ... ] = field (repr = False , compare = False )
293293
294294 @staticmethod
295295 def is_template (value : str ) -> bool :
@@ -319,17 +319,19 @@ def parse(
319319 template : str ,
320320 * ,
321321 max_length : int = DEFAULT_MAX_TEMPLATE_LENGTH ,
322- max_expressions : int = DEFAULT_MAX_EXPRESSIONS ,
322+ max_variables : int = DEFAULT_MAX_VARIABLES ,
323323 ) -> UriTemplate :
324324 """Parse a URI template string.
325325
326326 Args:
327327 template: An RFC 6570 URI template.
328328 max_length: Maximum permitted length of the template string.
329329 Guards against resource exhaustion.
330- max_expressions: Maximum number of ``{...}`` expressions
331- permitted. Guards against pathological inputs that could
332- produce expensive regexes.
330+ max_variables: Maximum number of variables permitted across
331+ all expressions. Counting variables rather than
332+ ``{...}`` expressions closes the gap where a single
333+ ``{v0,v1,...,vN}`` expression packs arbitrarily many
334+ variables under one expression count.
333335
334336 Raises:
335337 InvalidUriTemplate: If the template is malformed, exceeds the
@@ -341,7 +343,7 @@ def parse(
341343 template = template ,
342344 )
343345
344- parts , variables = _parse (template , max_expressions = max_expressions )
346+ parts , variables = _parse (template , max_variables = max_variables )
345347
346348 # Trailing {?...}/{&...} expressions are matched leniently via
347349 # parse_qs rather than the scan: order-agnostic, partial, ignores
@@ -352,12 +354,12 @@ def parse(
352354
353355 return cls (
354356 template = template ,
355- _parts = parts ,
356- _variables = variables ,
357- _prefix = prefix ,
357+ _parts = tuple ( parts ) ,
358+ _variables = tuple ( variables ) ,
359+ _prefix = tuple ( prefix ) ,
358360 _greedy = greedy ,
359- _suffix = suffix ,
360- _query_variables = query_vars ,
361+ _suffix = tuple ( suffix ) ,
362+ _query_variables = tuple ( query_vars ) ,
361363 )
362364
363365 @property
@@ -680,7 +682,7 @@ def _split_query_tail(parts: list[_Part]) -> tuple[list[_Part], list[Variable]]:
680682 return parts [:split ], query_vars
681683
682684
683- def _parse (template : str , * , max_expressions : int ) -> tuple [list [_Part ], list [Variable ]]:
685+ def _parse (template : str , * , max_variables : int ) -> tuple [list [_Part ], list [Variable ]]:
684686 """Split a template into an ordered sequence of literals and expressions.
685687
686688 Walks the string, alternating between collecting literal runs and
@@ -694,7 +696,6 @@ def _parse(template: str, *, max_expressions: int) -> tuple[list[_Part], list[Va
694696 """
695697 parts : list [_Part ] = []
696698 variables : list [Variable ] = []
697- expression_count = 0
698699 i = 0
699700 n = len (template )
700701
@@ -719,18 +720,17 @@ def _parse(template: str, *, max_expressions: int) -> tuple[list[_Part], list[Va
719720 position = brace ,
720721 )
721722
722- expression_count += 1
723- if expression_count > max_expressions :
724- raise InvalidUriTemplate (
725- f"Template exceeds maximum of { max_expressions } expressions" ,
726- template = template ,
727- )
728-
729723 # Delegate body (between braces, exclusive) to the expression parser.
730724 expr = _parse_expression (template , template [brace + 1 : end ], brace )
731725 parts .append (expr )
732726 variables .extend (expr .variables )
733727
728+ if len (variables ) > max_variables :
729+ raise InvalidUriTemplate (
730+ f"Template exceeds maximum of { max_variables } variables" ,
731+ template = template ,
732+ )
733+
734734 # Advance past the closing brace.
735735 i = end + 1
736736
@@ -903,7 +903,7 @@ def _partition_greedy(atoms: list[_Atom], template: str) -> tuple[list[_Atom], V
903903 return atoms [:greedy_idx ], greedy .var , atoms [greedy_idx + 1 :]
904904
905905
906- def _scan_suffix (atoms : list [_Atom ], uri : str , end : int ) -> tuple [dict [str , str | list [str ]], int ] | None :
906+ def _scan_suffix (atoms : Sequence [_Atom ], uri : str , end : int ) -> tuple [dict [str , str | list [str ]], int ] | None :
907907 """Scan atoms right-to-left from ``end``, returning captures and start position.
908908
909909 Each bounded variable takes the minimum span that lets its
@@ -973,7 +973,9 @@ def _scan_suffix(atoms: list[_Atom], uri: str, end: int) -> tuple[dict[str, str
973973 return result , pos
974974
975975
976- def _scan_prefix (atoms : list [_Atom ], uri : str , start : int , limit : int ) -> tuple [dict [str , str | list [str ]], int ] | None :
976+ def _scan_prefix (
977+ atoms : Sequence [_Atom ], uri : str , start : int , limit : int
978+ ) -> tuple [dict [str , str | list [str ]], int ] | None :
977979 """Scan atoms left-to-right from ``start``, not exceeding ``limit``.
978980
979981 Each bounded variable takes the minimum span that lets its
0 commit comments