1+ from __future__ import annotations
2+
3+ from dataclasses import dataclass
14from typing import Any
5+ from typing import Dict
6+ from typing import Optional
27
38from parse import Parser
49
@@ -18,16 +23,115 @@ class PathParser(Parser): # type: ignore
1823 def __init__ (
1924 self , pattern : str , pre_expression : str = "" , post_expression : str = ""
2025 ) -> None :
26+ self ._orig_to_safe : Dict [str , str ] = {}
27+ self ._safe_to_orig : Dict [str , str ] = {}
28+ self ._safe_suffix_counters : Dict [str , int ] = {}
2129 extra_types = {
2230 self .parse_path_parameter .name : self .parse_path_parameter
2331 }
24- super ().__init__ (pattern , extra_types )
32+ sanitized_pattern = self ._sanitize_pattern (pattern )
33+ super ().__init__ (sanitized_pattern , extra_types )
2534 self ._expression : str = (
2635 pre_expression + self ._expression + post_expression
2736 )
2837
38+ def search (self , string : str , pos : int = 0 , endpos : Optional [int ] = None ) -> Any :
39+ result = super ().search (string , pos = pos , endpos = endpos )
40+ if not result :
41+ return result
42+ return _RemappedResult (result , self ._safe_to_orig )
43+
44+ def parse (self , string : str , pos : int = 0 , endpos : Optional [int ] = None ) -> Any :
45+ result = super ().parse (string , pos = pos , endpos = endpos )
46+ if not result :
47+ return result
48+ return _RemappedResult (result , self ._safe_to_orig )
49+
50+ def _get_safe_field_name (self , original : str ) -> str :
51+ existing = self ._orig_to_safe .get (original )
52+ if existing is not None :
53+ return existing
54+
55+ safe_parts = []
56+ for ch in original :
57+ if ch == "_" or ch .isalnum ():
58+ safe_parts .append (ch )
59+ else :
60+ safe_parts .append (f"__{ ord (ch ):x} __" )
61+
62+ safe = "" .join (safe_parts ) or "p"
63+ # `parse` and Python `re` named groups are most reliable when the group name
64+ # starts with a letter.
65+ if not safe [0 ].isalpha ():
66+ safe = f"p_{ safe } "
67+
68+ # Ensure uniqueness across fields within this parser
69+ if safe in self ._safe_to_orig and self ._safe_to_orig [safe ] != original :
70+ base = safe
71+ suffix = self ._safe_suffix_counters .get (base , 1 )
72+ while True :
73+ candidate = f"{ base } __{ suffix } "
74+ if candidate not in self ._safe_to_orig :
75+ safe = candidate
76+ self ._safe_suffix_counters [base ] = suffix + 1
77+ break
78+ suffix += 1
79+
80+ self ._orig_to_safe [original ] = safe
81+ self ._safe_to_orig [safe ] = original
82+ return safe
83+
84+ def _sanitize_pattern (self , pattern : str ) -> str :
85+ # Pre-sanitize field names inside `{...}` before `parse` processes them.
86+ # This ensures special characters (e.g. `~`) and digit-leading names are
87+ # treated as named fields instead of literals or positional groups.
88+ if "{" not in pattern :
89+ return pattern
90+
91+ out : list [str ] = []
92+ i = 0
93+ n = len (pattern )
94+ while i < n :
95+ ch = pattern [i ]
96+ if ch != "{" :
97+ out .append (ch )
98+ i += 1
99+ continue
100+
101+ end = pattern .find ("}" , i + 1 )
102+ if end == - 1 :
103+ out .append (ch )
104+ i += 1
105+ continue
106+
107+ original = pattern [i + 1 : end ]
108+ safe = self ._get_safe_field_name (original )
109+ out .append ("{" )
110+ out .append (safe )
111+ out .append ("}" )
112+ i = end + 1
113+
114+ return "" .join (out )
115+
29116 def _handle_field (self , field : str ) -> Any :
30117 # handle as path parameter field
31- field = field [1 :- 1 ]
32- path_parameter_field = "{%s:PathParameter}" % field
118+ safe_field = field [1 :- 1 ]
119+ path_parameter_field = "{%s:PathParameter}" % safe_field
33120 return super ()._handle_field (path_parameter_field )
121+
122+
123+ @dataclass (frozen = True )
124+ class _RemappedResult :
125+ _result : Any
126+ _safe_to_orig : Dict [str , str ]
127+
128+ @property
129+ def named (self ) -> Dict [str , Any ]:
130+ named = getattr (self ._result , "named" , {})
131+ return {self ._safe_to_orig .get (k , k ): v for k , v in named .items ()}
132+
133+ def __bool__ (self ) -> bool :
134+ return bool (self ._result )
135+
136+ def __getattr__ (self , item : str ) -> Any :
137+ return getattr (self ._result , item )
0 commit comments