Skip to content

Commit a27c710

Browse files
committed
Re-enable retrieving exact results from the package repository if they weren't in the DB
1 parent cb3d27e commit a27c710

File tree

3 files changed

+101
-48
lines changed

3 files changed

+101
-48
lines changed

simple_repository_browser/_search.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@ def normalise_name(name: str) -> str:
5151
class SQLBuilder:
5252
"""Immutable SQL WHERE and ORDER BY clauses with parameters."""
5353

54-
where_clause: str = ""
55-
where_params: tuple[typing.Any, ...] = ()
56-
order_clause: str = "ORDER BY canonical_name"
57-
order_params: tuple[typing.Any, ...] = ()
54+
where_clause: str
55+
where_params: tuple[typing.Any, ...]
56+
order_clause: str
57+
order_params: tuple[typing.Any, ...]
58+
search_context: SearchContext
5859

5960
@property
6061
def where_params_count(self) -> int:
@@ -92,24 +93,37 @@ def with_order(self, clause: str, params: tuple[typing.Any, ...]) -> SQLBuilder:
9293
class SearchContext:
9394
"""Context collected during WHERE clause building."""
9495

95-
exact_names: set[str] = dataclasses.field(default_factory=set)
96-
fuzzy_patterns: set[str] = dataclasses.field(default_factory=set)
96+
exact_names: tuple[str, ...] = ()
97+
fuzzy_patterns: tuple[str, ...] = ()
9798

9899
def with_exact_name(self, name: str) -> SearchContext:
99100
"""Add an exact name match."""
100-
return dataclasses.replace(self, exact_names=self.exact_names | {name})
101+
if name in self.exact_names:
102+
return self
103+
else:
104+
return dataclasses.replace(self, exact_names=self.exact_names + (name,))
101105

102106
def with_fuzzy_pattern(self, pattern: str) -> SearchContext:
103107
"""Add a fuzzy search pattern."""
104-
return dataclasses.replace(self, fuzzy_patterns=self.fuzzy_patterns | {pattern})
108+
if pattern in self.fuzzy_patterns:
109+
return self
110+
else:
111+
return dataclasses.replace(
112+
self, fuzzy_patterns=self.fuzzy_patterns + (pattern,)
113+
)
105114

106115
def merge(self, other: SearchContext) -> SearchContext:
107116
"""Merge contexts from multiple terms (for OR/AND)."""
108-
return dataclasses.replace(
109-
self,
110-
exact_names=self.exact_names | other.exact_names,
111-
fuzzy_patterns=self.fuzzy_patterns | other.fuzzy_patterns,
117+
names = self.exact_names + tuple(
118+
name for name in other.exact_names if name not in self.exact_names
112119
)
120+
patterns = self.fuzzy_patterns + tuple(
121+
pattern
122+
for pattern in other.fuzzy_patterns
123+
if pattern not in self.fuzzy_patterns
124+
)
125+
126+
return dataclasses.replace(self, exact_names=names, fuzzy_patterns=patterns)
113127

114128

115129
class SearchCompiler:
@@ -127,11 +141,16 @@ def compile(cls, terms: Term | tuple[Term, ...]) -> SQLBuilder:
127141
terms = (terms,) if terms else ()
128142

129143
if len(terms) == 0:
130-
return SQLBuilder()
144+
return SQLBuilder(
145+
where_clause="",
146+
where_params=(),
147+
order_clause="",
148+
order_params=(),
149+
search_context=SearchContext(),
150+
)
151+
assert len(terms) == 1
131152

132153
# Build WHERE clause and collect context
133-
# Note: Current grammar only produces single terms
134-
# TODO: If this changes, we'll need to handle term combinations
135154
context = SearchContext()
136155
where_clause, where_params, final_context = cls._visit_term(terms[0], context)
137156

@@ -143,6 +162,7 @@ def compile(cls, terms: Term | tuple[Term, ...]) -> SQLBuilder:
143162
where_params=where_params,
144163
order_clause=order_clause,
145164
order_params=order_params,
165+
search_context=final_context,
146166
)
147167

148168
@classmethod

simple_repository_browser/model.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -155,21 +155,27 @@ async def project_query(
155155
# Convert results to SearchResultItem objects
156156
results = [SearchResultItem(*result) for result in results]
157157

158-
# TODO: Re-enable package repository lookup for missing exact matches
159-
# Check if single_name_proposal is already in the results
160-
# if single_name_proposal and page == 1:
161-
# exact_found = any(r.canonical_name == single_name_proposal for r in results)
162-
# if not exact_found:
163-
# # Not in results, check if it exists in repository
164-
# try:
165-
# await self.source.get_project_page(single_name_proposal)
166-
# # Package exists in repository! Add it to the beginning
167-
# results.insert(
168-
# 0, SearchResultItem(canonical_name=single_name_proposal)
169-
# )
170-
# n_results += 1
171-
# except PackageNotFoundError:
172-
# pass
158+
# If the search was for a specific name, then make sure we return it if
159+
# it is in the package repository.
160+
if page == 1:
161+
result_names = {r.canonical_name for r in results}
162+
missing = tuple(
163+
name
164+
for name in sql_builder.search_context.exact_names
165+
if name not in result_names
166+
)
167+
if missing:
168+
for name_proposal in reversed(missing):
169+
# Not in results, check if it exists in repository
170+
try:
171+
await self.source.get_project_page(name_proposal)
172+
# Package exists in repository! Add it to the beginning
173+
results.insert(
174+
0, SearchResultItem(canonical_name=name_proposal)
175+
)
176+
n_results += 1
177+
except PackageNotFoundError:
178+
pass
173179

174180
return QueryResultModel(
175181
search_query=query,

simple_repository_browser/tests/test_search.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,20 @@ def test_invalid_query(query, expected_exception):
192192
class MockSimpleRepository:
193193
"""Mock repository for testing search functionality."""
194194

195+
def __init__(self, available_packages=None):
196+
# Packages that exist in the repository but not in the database
197+
self.available_packages = available_packages or set()
198+
195199
async def get_project_page(self, name: str):
196-
"""Mock project page retrieval - not needed for search ordering tests."""
197-
raise Exception(f"Project {name} not found")
200+
"""Mock project page retrieval."""
201+
from simple_repository.errors import PackageNotFoundError
202+
203+
if name in self.available_packages:
204+
# Return a mock project detail for testing
205+
from simple_repository.model import Meta, ProjectDetail
206+
207+
return ProjectDetail(meta=Meta("1.0"), name=name, files=())
208+
raise PackageNotFoundError(f"Project {name} not found")
198209

199210

200211
@pytest.fixture
@@ -386,22 +397,7 @@ async def test_complex_mixed_query(test_model):
386397
"""Test complex mixed query with multiple exact names."""
387398
result = await test_model.project_query("numpy OR scipy", page_size=10, page=1)
388399

389-
names = [item.canonical_name for item in result["results"]]
390-
391-
# Should include both families
392-
assert "numpy" in names
393-
assert "scipy" in names
394-
assert "numpy-image" in names
395-
assert "scipy2" in names
396-
397-
# Exact matches should come before their related packages
398-
numpy_idx = names.index("numpy")
399-
scipy_idx = names.index("scipy")
400-
numpy_image_idx = names.index("numpy-image")
401-
scipy2_idx = names.index("scipy2")
402-
403-
assert numpy_idx < numpy_image_idx
404-
assert scipy_idx < scipy2_idx
400+
assert_order(["numpy", "scipy", "numpy-image", "scipy2"], result["results"])
405401

406402

407403
@pytest.mark.asyncio
@@ -429,3 +425,34 @@ async def test_empty_results(test_model):
429425

430426
assert result["results"] == []
431427
assert result["results_count"] == 0
428+
429+
430+
@pytest.mark.asyncio
431+
async def test_injected_results_when_not_in_db(test_model):
432+
assert isinstance(test_model.source, MockSimpleRepository)
433+
test_model.source.available_packages = [
434+
"jingo",
435+
"wibble",
436+
"wibbleof",
437+
"bongo",
438+
"bongo-bong",
439+
]
440+
result = await test_model.project_query(
441+
"jingo OR wibble* OR boNgo OR bongo_Bong OR numpy OR totallyMissing",
442+
page_size=10,
443+
page=1,
444+
)
445+
# result = await test_model.project_query("jingo OR numpy", page_size=10, page=1)
446+
447+
names = [item.canonical_name for item in result["results"]]
448+
449+
assert "wibble" not in names
450+
assert_order(
451+
["jingo", "bongo", "bongo-bong", "numpy"],
452+
result["results"],
453+
)
454+
# Check that the one result that came from the db actually has the summary.
455+
[numpy_result] = [
456+
item for item in result["results"] if item.canonical_name == "numpy"
457+
]
458+
assert "Fundamental" in numpy_result.summary

0 commit comments

Comments
 (0)