Skip to content

Commit 0c75699

Browse files
committed
fix(search): keep a limit the caller wrote into the ask query
- semantic_search appended |limit= unconditionally, and SMW honours the last limit, so '[[Category:Item]]|limit=2' was sent as ...|limit=1000 - get_query_limit() reads the limit a query sets itself, ignoring conditions, printouts and printout parameters - the default is appended only to a query that sets no limit - the truncation warning now compares against the limit in force - 'limit=0' no longer warns about truncation
1 parent 3a45142 commit 0c75699

2 files changed

Lines changed: 139 additions & 4 deletions

File tree

‎src/osw/wiki_tools.py‎

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import getpass
2+
import re
23
import warnings
34
from typing import Dict, List, Optional, Tuple, Union
45

@@ -137,6 +138,8 @@ class SearchParam(OswBaseModel):
137138
parallel: Optional[bool] = None # is set to true if query is a list longer than 5
138139
debug: Optional[bool] = False
139140
limit: Optional[int] = 1000
141+
"""the result limit to apply. Ignored by semantic_search for a query that
142+
sets 'limit=' itself, since SMW honours the last limit in the query string"""
140143
return_json: Optional[bool] = False
141144

142145
def __init__(self, **data):
@@ -244,6 +247,36 @@ def _ask_results_as_dict(results: Union[dict, list]) -> dict:
244247
}
245248

246249

250+
CONDITION_PATTERN = re.compile(r"\[\[.*?\]\]", re.DOTALL)
251+
"""matches an SMW query condition, which may contain '|' as the disjunction
252+
operator and must therefore be removed before the parameters are split"""
253+
LIMIT_PARAM_PATTERN = re.compile(r"^limit\s*=\s*(\d+)$", re.IGNORECASE)
254+
"""matches a limit parameter of an SMW query, e.g. 'limit=100'"""
255+
256+
257+
def get_query_limit(query: str) -> Optional[int]:
258+
"""Returns the limit an SMW ask query sets itself, None if it sets none
259+
260+
Parameters
261+
----------
262+
query :
263+
an SMW ask query string, e.g. '[[Category:Item]]|?Name|limit=2'
264+
265+
Returns
266+
-------
267+
result:
268+
the limit the query asks for, or None if the query does not set one or
269+
sets one that is not a number
270+
"""
271+
limit = None
272+
for parameter in CONDITION_PATTERN.sub("", query).split("|"):
273+
match = LIMIT_PARAM_PATTERN.match(parameter.strip())
274+
if match:
275+
# SMW honours the last limit in the query string
276+
limit = int(match.group(1))
277+
return limit
278+
279+
247280
def semantic_search(
248281
site: mwclient.client.Site, query: Union[str, List[str], SearchParam]
249282
) -> Union[List[str], List[dict]]:
@@ -254,7 +287,9 @@ def semantic_search(
254287
site :
255288
Site object from mwclient lib
256289
query :
257-
(List of) query text(s) or instance of SearchParam
290+
(List of) query text(s) or instance of SearchParam. A query that sets
291+
``limit=`` itself keeps that limit; ``SearchParam.limit`` is only
292+
appended to a query that does not.
258293
259294
Returns
260295
-------
@@ -268,7 +303,12 @@ def semantic_search(
268303

269304
def semantic_search_(single_query):
270305
page_list = list()
271-
single_query += f"|limit={query.limit}"
306+
# SMW honours the last limit in the query string, so appending the
307+
# default would silently override a limit the caller wrote themselves
308+
limit = get_query_limit(single_query)
309+
if limit is None:
310+
limit = query.limit
311+
single_query += f"|limit={limit}"
272312
result = site.api("ask", query=single_query, format="json")
273313
results = _ask_results_as_dict(result["query"]["results"])
274314
n = len(results)
@@ -277,10 +317,12 @@ def semantic_search_(single_query):
277317
print(f"Query '{single_query}' returned no results")
278318
else:
279319
print(f"Query '{single_query}' returned {n} results")
280-
if n >= query.limit:
320+
# 'limit=0' asks for no results at all, e.g. with a count format, so
321+
# meeting it says nothing about truncation
322+
if limit and n >= limit:
281323
warnings.warn(
282324
f"Query '{single_query}' returned {n} results, which meets the "
283-
f"requested limit of {query.limit}. Results are truncated - raise "
325+
f"requested limit of {limit}. Results are truncated - raise "
284326
f"the limit or page through with '|offset=' to retrieve the "
285327
f"remainder."
286328
)

‎tests/test_wiki_tools.py‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,99 @@ def test_semantic_search_exists_drop_warning():
207207
assert out == ["Item:OSW1"]
208208

209209

210+
@pytest.mark.parametrize(
211+
"query, expected",
212+
[
213+
("[[HasType::Category:Item]]", None),
214+
("[[HasType::Category:Item]]|limit=2", 2),
215+
("[[HasType::Category:Item]]|?Name|limit=2|offset=5", 2),
216+
("[[HasType::Category:Item]]| limit = 2 ", 2),
217+
("[[HasType::Category:Item]]|Limit=2", 2),
218+
# SMW honours the last one, so that is the effective limit
219+
("[[HasType::Category:Item]]|limit=2|limit=7", 7),
220+
# a limit inside a condition is a value, not a parameter
221+
("[[HasText::limit=2]]", None),
222+
("[[HasText::a||limit=2]]", None),
223+
# a printout named 'limit', not the parameter
224+
("[[HasType::Category:Item]]|?limit", None),
225+
# a printout parameter, which limits that printout and not the query
226+
("[[HasType::Category:Item]]|?Has subobject|+limit=3", None),
227+
# not a number, so there is no limit to honour
228+
("[[HasType::Category:Item]]|limit=all", None),
229+
],
230+
)
231+
def test_get_query_limit(query, expected):
232+
assert wt.get_query_limit(query) == expected
233+
234+
235+
def test_semantic_search_appends_the_default_limit():
236+
site = MagicMock()
237+
site.api.return_value = _ask_result("Item:OSW1")
238+
239+
wt.semantic_search(site, "[[HasType::Category:Item]]")
240+
241+
assert site.api.call_args.kwargs["query"] == (
242+
"[[HasType::Category:Item]]|limit=1000"
243+
)
244+
245+
246+
def test_semantic_search_keeps_a_limit_the_caller_wrote():
247+
"""Appending the default would override it, since SMW honours the last one."""
248+
site = MagicMock()
249+
site.api.return_value = _ask_result("Item:OSW1")
250+
251+
wt.semantic_search(site, "[[HasType::Category:Item]]|limit=2")
252+
253+
assert site.api.call_args.kwargs["query"] == "[[HasType::Category:Item]]|limit=2"
254+
255+
256+
def test_semantic_search_query_limit_beats_the_search_param_limit():
257+
site = MagicMock()
258+
site.api.return_value = _ask_result("Item:OSW1")
259+
260+
wt.semantic_search(
261+
site, wt.SearchParam(query="[[HasType::Category:Item]]|limit=2", limit=500)
262+
)
263+
264+
assert site.api.call_args.kwargs["query"] == "[[HasType::Category:Item]]|limit=2"
265+
266+
267+
def test_semantic_search_truncation_warning_uses_the_query_limit():
268+
"""The caller's limit is the one the results were truncated at."""
269+
titles = [f"Item:OSW{i}" for i in range(2)]
270+
site = MagicMock()
271+
site.api.return_value = _ask_result(*titles)
272+
273+
with pytest.warns(UserWarning, match="requested limit of 2"):
274+
out = wt.semantic_search(site, "[[HasType::Category:Item]]|limit=2")
275+
276+
assert sorted(out) == sorted(titles)
277+
278+
279+
def test_semantic_search_no_truncation_warning_for_a_zero_limit():
280+
"""'limit=0' asks for no results, so meeting it is not truncation."""
281+
site = MagicMock()
282+
site.api.return_value = _ask_result_empty()
283+
284+
with warnings.catch_warnings(record=True) as caught:
285+
warnings.simplefilter("always")
286+
wt.semantic_search(site, "[[HasType::Category:Item]]|limit=0")
287+
288+
assert not any("truncated" in str(w.message) for w in caught)
289+
290+
291+
def test_semantic_search_no_truncation_warning_below_the_query_limit():
292+
titles = [f"Item:OSW{i}" for i in range(2)]
293+
site = MagicMock()
294+
site.api.return_value = _ask_result(*titles)
295+
296+
with warnings.catch_warnings(record=True) as caught:
297+
warnings.simplefilter("always")
298+
wt.semantic_search(site, "[[HasType::Category:Item]]|limit=50")
299+
300+
assert not any("truncated" in str(w.message) for w in caught)
301+
302+
210303
def _prefixsearch_result(*titles):
211304
"""Build a minimal MediaWiki ``prefixsearch`` API result dict."""
212305
return {

0 commit comments

Comments
 (0)