def test_expand_inf_2(): with pytest.raises(ValueError) as err: expand( "content:(\"% 1/7,%\" or \"1/7,%\") and not content:(\"% 1/7\" or \"% 1/7 %\" or \"1/7 %\" or \"1/7\") " "and not type:(qualify or nominate)") assert "expanded infinitely" in str(err)
def test_expand_inf(): with pytest.raises(ValueError) as err: expand( "type:(rank and love and qualify and disqualify and remove_from_loved) mode:(osu and taiko and catch and mania)" ) assert "expanded infinitely" in str(err)
def test(self, _filter: str, obj_or_dissection: Union[object, List[str]]) -> bool: """Tests the given filter against the dissection of an object. Returns whether the key:value pairs match logically. Case insensitive. That is, if all AND sections within any OR section evaluate to True, in the expanded filter.""" if isinstance(obj_or_dissection, list): dissection = obj_or_dissection else: dissection = self.dissect(obj_or_dissection) _filter = _filter.lower() for or_split, _ in split_unescaped(expand(_filter), OR_GATES): passes_and = True for and_split, _ in split_unescaped(or_split, AND_GATES): without_not_gate, not_gate = extract_not(and_split) if not_gate: if self.test_kvpair(without_not_gate, dissection): passes_and = False break else: if not self.test_kvpair(and_split, dissection): passes_and = False break if passes_and: return True return False
def get_invalid_words(_filter: str) -> Generator[str, None, None]: """Returns all space-separated instances of text, which are neither key-value pairs nor logical gates, in the given filter.""" for split, _ in split_unescaped(expand(_filter), (" ",)): is_key_value_pair = re.match(KEY_VALUE_PATTERN, split) is_logical_gate = split.lower() in map(lambda gate: gate.replace(" ", ""), NOT_GATES + AND_GATES + OR_GATES) if not is_key_value_pair and not is_logical_gate: yield split
def get_missing_gate(_filter: str) -> Generator[str, None, None]: """Returns a tuple of the first two space-separated instances of text, which have no gate between them, in the given filter.""" was_gate = False prev_split = None for split, _ in split_unescaped(expand(_filter), (" ",)): is_key_value_pair = re.match(KEY_VALUE_PATTERN, split) is_logical_gate = split.lower() in map(lambda gate: gate.replace(" ", ""), NOT_GATES + AND_GATES + OR_GATES) if prev_split and is_key_value_pair and not was_gate: return (prev_split, split) was_gate = is_logical_gate prev_split = split
def filter_to_sql(_filter: str) -> Tuple[str, tuple]: """Returns a tuple of the filter converted to an SQL WHERE clause and the inputs to the WHERE clause (e.g. ("type=%s", ("nominate",)) ), for use with the scraper database.""" if not _filter: # Without a filter, we simply let everything through. return ("TRUE", ()) if not is_valid(_filter, filter_context): raise ValueError("Received an invalid filter; cannot convert to sql.") converted_words = [] converted_values = [] for word, _ in split_unescaped(expand(_filter), (" ",)): # Convert gate symbols in the filter (e.g. "&", "!", "and", "|") to "AND", "OR", and "NOT". if any(map(lambda gate: word.lower() == gate.strip().lower(), AND_GATES)): word = "AND" if any(map(lambda gate: word.lower() == gate.strip().lower(), OR_GATES)): word = "OR" if any(map(lambda gate: word.lower() == gate.strip().lower(), NOT_GATES)): word = "NOT" if word in ["AND", "OR", "NOT"]: converted_words.append(word) continue key, value = next(get_key_value_pairs(word)) tag = filter_context.get_tag(key) if not tag: continue values = [] # Support type aliases (e.g. "resolve" should be converted to "issue-resolve"). if key.lower() == "type": for _type in TYPE_ALIASES: if value.lower() in TYPE_ALIASES[_type]: values.append(_type) # Support group aliases (e.g. "nat" should be converted to "7"). if key.lower() == "group": for group in GROUP_ALIASES: if value.lower() in GROUP_ALIASES[group]: values.append(group) if not values: # Our value is not an alias, so we can use it directly. values.append(value) if len(values) > 1: converted_words.append("(" + " OR ".join([TAG_TO_SQL[tag]] * len(values)) + ")") else: converted_words.append(TAG_TO_SQL[tag]) converted_values.extend(values) return (" ".join(converted_words), tuple(converted_values))
def get_invalid_gates(_filter: str) -> Generator[str, None, None]: """Returns a generator of invalid gates from the given filter (i.e. starting or ending the filter with one).""" expanded_filter = expand(_filter).strip() for gate in map(lambda g: g.strip(), (AND_GATES + OR_GATES)): if expanded_filter.startswith(gate) or expanded_filter.endswith(gate): yield gate
def get_key_value_pairs(_filter: str) -> Generator[Tuple[str, str], None, None]: """Returns a generator of key-value pair tuples from the given filter.""" expansion = expand(_filter) for match in re.finditer(KEY_VALUE_PATTERN, expansion): yield (match.group(1), match.group(2).strip("\""))
def test_expand_redundancy(): assert expand("(type:(qualify))") == "type:qualify"
def test_expand_complex_capital(): assert expand("A OR E AND NOT (B AND (C OR NOT D))" ) == "A or E and not B or E and not C and E and D"
def test_expand_complex(): assert expand("A or E and not (B and (C or not D))" ) == "A or E and not B or E and not C and E and D"
def test_expand_not_before_type(): assert expand("not type:(nominate or qualify)" ) == "not type:nominate and not type:qualify"
def test_expand_not(): assert expand("type:not (nominate or qualify)" ) == "not type:nominate and not type:qualify"
def test_expand_multiple_and(): assert ( expand("type:(nominate or qualify) and user:(123 or 456)") == "type:nominate and user:123 or type:nominate and user:456 or type:qualify and user:123 or type:qualify and user:456" )
def test_expand_and_quotes(): assert (expand( "type:(nominate or qualify) and user:\"some user with and in their name\"" ) == "type:nominate and user:\"some user with and in their name\" or type:qualify and user:\"some user with and in their name\"" )
def test_expand_and(): assert expand( "type:(nominate or qualify) and user:123" ) == "type:nominate and user:123 or type:qualify and user:123"
def test_expand_nested_leading_or(): assert expand("type:(not B or (not C and D))" ) == "not type:B or not type:C and type:D"
def test_expand(): assert expand( "type:(nominate or qualify)") == "type:nominate or type:qualify"