Skip to content

Commit 39aeac9

Browse files
authored
parse_query: strip query (#414)
* parse_query: strip query
1 parent 5aeecdc commit 39aeac9

File tree

2 files changed

+189
-2
lines changed

2 files changed

+189
-2
lines changed

adminapi/parse.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111

1212
def parse_query(term, hostname=None): # NOQA: C901
13-
# Ignore newlines to allow queries across multiple lines
14-
term = term.replace('\n', '')
13+
# Replace newlines with spaces to allow queries across multiple lines
14+
term = term.replace('\n', ' ').strip()
1515

1616
parsed_args = parse_function_string(term, strict=True)
1717
if not parsed_args:

adminapi/tests/test_parse.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import unittest
2+
3+
from adminapi.datatype import DatatypeError
4+
from adminapi.filters import (
5+
BaseFilter,
6+
Regexp,
7+
Any,
8+
GreaterThan,
9+
)
10+
from adminapi.parse import parse_query, parse_function_string
11+
12+
13+
def assert_filters_equal(test_case, result, expected):
14+
"""Compare filter dictionaries by their repr, which includes structure and values."""
15+
test_case.assertEqual(sorted(result.keys()), sorted(expected.keys()))
16+
for key in expected:
17+
test_case.assertEqual(repr(result[key]), repr(expected[key]))
18+
19+
20+
class TestParseQuery(unittest.TestCase):
21+
def test_simple_attribute(self):
22+
result = parse_query("hostname=web01")
23+
expected = {"hostname": BaseFilter("web01")}
24+
assert_filters_equal(self, result, expected)
25+
26+
def test_whitespace_handling(self):
27+
result = parse_query(" hostname=test ")
28+
expected = {"hostname": BaseFilter("test")}
29+
assert_filters_equal(self, result, expected)
30+
31+
def test_multiple_attributes(self):
32+
result = parse_query("hostname=web01 state=online")
33+
expected = {
34+
"hostname": BaseFilter("web01"),
35+
"state": BaseFilter("online"),
36+
}
37+
assert_filters_equal(self, result, expected)
38+
39+
def test_hostname_shorthand(self):
40+
result = parse_query("web01 state=online")
41+
expected = {
42+
"hostname": BaseFilter("web01"),
43+
"state": BaseFilter("online"),
44+
}
45+
assert_filters_equal(self, result, expected)
46+
47+
def test_hostname_shorthand_with_regexp(self):
48+
# Hostname shortcuts automatically detect regex patterns
49+
result = parse_query("web.*")
50+
expected = {"hostname": Regexp("web.*")}
51+
assert_filters_equal(self, result, expected)
52+
53+
def test_regexp_pattern_as_literal(self):
54+
# Regex patterns in attribute values are treated as literals
55+
# Use Regexp() function for actual regex filtering
56+
result = parse_query("hostname=web.*")
57+
expected = {"hostname": BaseFilter("web.*")}
58+
assert_filters_equal(self, result, expected)
59+
60+
def test_explicit_regexp_function(self):
61+
# Use explicit Regexp() function for regex filtering
62+
result = parse_query("hostname=Regexp(web.*)")
63+
expected = {"hostname": Regexp("web.*")}
64+
assert_filters_equal(self, result, expected)
65+
66+
def test_function_filter(self):
67+
result = parse_query("num_cores=GreaterThan(4)")
68+
expected = {"num_cores": GreaterThan(4)}
69+
assert_filters_equal(self, result, expected)
70+
71+
def test_function_with_multiple_args(self):
72+
result = parse_query("hostname=Any(web01 web02)")
73+
expected = {"hostname": Any("web01", "web02")}
74+
assert_filters_equal(self, result, expected)
75+
76+
def test_empty_query(self):
77+
result = parse_query("")
78+
self.assertEqual(result, {})
79+
80+
def test_whitespace_only_query(self):
81+
result = parse_query(" ")
82+
self.assertEqual(result, {})
83+
84+
def test_newline_in_query(self):
85+
result = parse_query("hostname=web01\nstate=online")
86+
expected = {
87+
"hostname": BaseFilter("web01"),
88+
"state": BaseFilter("online"),
89+
}
90+
assert_filters_equal(self, result, expected)
91+
92+
def test_any_filter_with_duplicate_hostname(self):
93+
# Hostname shorthand triggers regex, but explicit attribute assignment doesn't
94+
result = parse_query("web.* hostname=db.*")
95+
expected = {"hostname": Any(BaseFilter("db.*"), Regexp("web.*"))}
96+
assert_filters_equal(self, result, expected)
97+
98+
def test_invalid_function(self):
99+
with self.assertRaisesRegex(DatatypeError, r"Invalid function InvalidFunc"):
100+
parse_query("hostname=InvalidFunc(test)")
101+
102+
def test_top_level_literal_error(self):
103+
with self.assertRaisesRegex(
104+
DatatypeError, r"Invalid term: Top level literals are not allowed"
105+
):
106+
parse_query("hostname=test value")
107+
108+
def test_top_level_function_as_hostname(self):
109+
# Function syntax without key is treated as hostname shorthand
110+
result = parse_query("GreaterThan(4)")
111+
expected = {"hostname": BaseFilter("GreaterThan(4)")}
112+
assert_filters_equal(self, result, expected)
113+
114+
def test_garbled_hostname_error(self):
115+
with self.assertRaisesRegex(DatatypeError, r"Garbled hostname: db01"):
116+
parse_query("web01", hostname="db01")
117+
118+
119+
class TestParseFunctionString(unittest.TestCase):
120+
def test_simple_key_value(self):
121+
result = parse_function_string("hostname=web01")
122+
self.assertEqual(result, [("key", "hostname"), ("literal", "web01")])
123+
124+
def test_quoted_string(self):
125+
result = parse_function_string('hostname="web 01"')
126+
self.assertEqual(result, [("key", "hostname"), ("literal", "web 01")])
127+
128+
result = parse_function_string("hostname='web 01'")
129+
self.assertEqual(result, [("key", "hostname"), ("literal", "web 01")])
130+
131+
result = parse_function_string('hostname="web\\"01"')
132+
self.assertEqual(result[1], ("literal", 'web\\"01'))
133+
134+
def test_function_call(self):
135+
result = parse_function_string("num_cores=GreaterThan(4)")
136+
expected = [
137+
("key", "num_cores"),
138+
("func", "GreaterThan"),
139+
("literal", 4),
140+
("endfunc", ""),
141+
]
142+
self.assertEqual(result, expected)
143+
144+
def test_nested_function(self):
145+
result = parse_function_string("attr=Func1(Func2(value))")
146+
self.assertEqual(result[0], ("key", "attr"))
147+
self.assertEqual(result[1], ("func", "Func1"))
148+
self.assertEqual(result[2], ("func", "Func2"))
149+
150+
def test_multiple_values(self):
151+
result = parse_function_string("host1 host2 host3")
152+
expected = [
153+
("literal", "host1"),
154+
("literal", "host2"),
155+
("literal", "host3"),
156+
]
157+
self.assertEqual(result, expected)
158+
159+
def test_datatype_conversion(self):
160+
result = parse_function_string("count=42")
161+
self.assertEqual(result, [("key", "count"), ("literal", 42)])
162+
163+
def test_unterminated_string(self):
164+
with self.assertRaisesRegex(DatatypeError, r"Unterminated string"):
165+
parse_function_string('hostname="web01', strict=True)
166+
167+
def test_invalid_escape(self):
168+
with self.assertRaisesRegex(DatatypeError, r"Invalid escape"):
169+
parse_function_string('hostname="web\\01"', strict=True)
170+
171+
def test_empty_string(self):
172+
result = parse_function_string("")
173+
self.assertEqual(result, [])
174+
175+
def test_whitespace_only(self):
176+
result = parse_function_string(" ")
177+
self.assertEqual(result, [])
178+
179+
def test_parentheses_handling(self):
180+
result = parse_function_string("func(a b)")
181+
expected = [
182+
("func", "func"),
183+
("literal", "a"),
184+
("literal", "b"),
185+
("endfunc", ""),
186+
]
187+
self.assertEqual(result, expected)

0 commit comments

Comments
 (0)