Parse a search expression into a Q object. Supports: - AND / OR (case-insensitive) between bare terms. - Double-quoted phrases (atomic; never split on operators). - Parenthetical grouping with arbitrary nesting. - Regex mode (entire raw value is treated as a single regex
(field_name, raw_value, use_regex=False, whole_words=False)
| 33 | |
| 34 | |
| 35 | def parse_text_query(field_name, raw_value, use_regex=False, whole_words=False): |
| 36 | """Parse a search expression into a Q object. |
| 37 | |
| 38 | Supports: |
| 39 | - AND / OR (case-insensitive) between bare terms. |
| 40 | - Double-quoted phrases (atomic; never split on operators). |
| 41 | - Parenthetical grouping with arbitrary nesting. |
| 42 | - Regex mode (entire raw value is treated as a single regex). |
| 43 | - Whole-word mode (each bare term anchored with word boundaries). |
| 44 | |
| 45 | A bare value with no operators is matched as a single phrase via |
| 46 | icontains (or regex / whole-word as configured). |
| 47 | """ |
| 48 | phrases = {} |
| 49 | |
| 50 | def extract_quoted(text): |
| 51 | def replacer(m): |
| 52 | key = f'\x00P{len(phrases)}\x00' |
| 53 | phrases[key] = m.group(1) |
| 54 | return key |
| 55 | return re.sub(r'"([^"]*)"', replacer, text) |
| 56 | |
| 57 | processed = extract_quoted(raw_value) |
| 58 | |
| 59 | def build_q(token): |
| 60 | return build_q_object(field_name, phrases.get(token, token), use_regex, whole_words) |
| 61 | |
| 62 | def parse_expression(expr): |
| 63 | expr = expr.strip() |
| 64 | |
| 65 | if '(' in expr: |
| 66 | paren_start = expr.rfind('(') |
| 67 | paren_end = expr.find(')', paren_start) |
| 68 | if paren_end == -1: |
| 69 | return Q() |
| 70 | |
| 71 | group_q = parse_expression(expr[paren_start + 1:paren_end]) |
| 72 | |
| 73 | before_str = expr[:paren_start].rstrip() |
| 74 | after_str = expr[paren_end + 1:].lstrip() |
| 75 | |
| 76 | before_op = '&' |
| 77 | if before_str.upper().endswith(' AND'): |
| 78 | before_str = before_str[:-4].rstrip() |
| 79 | elif before_str.upper().endswith(' OR'): |
| 80 | before_str = before_str[:-3].rstrip() |
| 81 | before_op = '|' |
| 82 | |
| 83 | after_op = '&' |
| 84 | after_upper = after_str.upper() |
| 85 | if after_upper.startswith('AND '): |
| 86 | after_str = after_str[4:].lstrip() |
| 87 | elif after_upper.startswith('OR '): |
| 88 | after_str = after_str[3:].lstrip() |
| 89 | after_op = '|' |
| 90 | |
| 91 | result = group_q |
| 92 | if before_str: |
no test coverage detected