Replace search_signal with an explicit search registry
- Dominant language
- Python
- Stars
- 30
- Forks
- 3
- Avg merge
- 9h 28m
- Merged PRs (30d)
- 42
Description
🤖 Written by Claude
## Summary
Search receivers are registered by connecting to a Django `Signal` (`search_signal` in `snpdb/search.py`). Signals are a notification mechanism, but here the signal is being used as a plugin registry plus dispatcher. An explicit registry would model what's actually happening, and is a small, self-contained change.
## What's happening today
`@search_receiver` (`snpdb/search.py:840`) wraps the search function and calls `search_signal.connect(search_func)`. Each app imports its own search modules from `AppConfig.ready()` for the import side-effect — e.g. `snpdb/apps.py:14` imports `snpdb.signals.lab_search`, `genes/apps.py` imports `genes.signals.gene_search`, etc.
`SearchInput.search()` (`snpdb/search.py:98`) then fires `search_signal.send_robust()` and sifts through the `(receiver, result)` tuples.
There are currently 45 `@search_receiver` functions across 27 modules in snpdb, genes, ontology, classification, annotation, analysis, patients, pedigree and seqauto.
The important observation is that the decoupling — apps not needing to know about each other — comes entirely from the `ready()` imports, not from the signal. Swapping the signal for an explicit registry keeps that property exactly as-is; no app needs a new import.
## What a registry would give us
**1. Search metadata lives with the search, and can be used before dispatch.**
Today `admin_only`, `preview_enabled()`, the `classify` gate and the `pattern` match are all evaluated *inside* the wrapper after the signal has already called it, because the signal can only pass `search_input`. With a registry entry holding that metadata, the dispatcher can filter first and only invoke the searches that apply.
**2. We can enumerate searches without running one.**
The search page needs the list of available search types and their `SearchExample`s. Right now the only way to get that is to fire the whole signal and read the `matched_pattern=False` responses back out — which is why `variantopedia/views.py:478` calls `search_data()` even when the search string is empty. A registry can be read directly.
**3. Simpler, typed dispatch.**
`send_robust()` returns `(receiver, result_or_exception)` tuples, so `SearchInput.search()` has to type-check every element and carries a branch commented *"note this doesn't happen as exceptions during search are handled by the search_receiver"*. That defensiveness only exists because of the signal's contract.
**4. Removes the weak-reference footgun.**
`Signal.connect()` defaults to `weak=True`, so registered searches stay alive only because the decorated module-level name holds the closure. Nothing is broken today, but it's a fragile property for something that behaves like a plugin registry. (Related: when `enabled=False`, the decorator returns `None`, so the decorated name becomes `None` rather than the function.)
**5. Per-search instrumentation becomes natural.**
With a real registry object it's easy to time each search and log/expose which ones dominate a query.
## Rough shape
```python
@dataclass(frozen=True)
class SearchReceiver:
func: Callable
search_type: PreviewCoordinator
pattern: Pattern
admin_only: bool
sub_name: Optional[str]
example: Optional[SearchExample]
match_strength: SearchResultMatchStrength
SEARCH_RECEIVERS: list[SearchReceiver] = []
```
`@search_receiver` appends a `SearchReceiver` instead of calling `search_signal.connect()`. The wrapper body that builds the `SearchResponse` stays as it is. `SearchInput.search()` iterates `SEARCH_RECEIVERS` instead of calling `send_robust()`.
The 45 receiver modules and the `ready()` imports are untouched — the change is contained to `snpdb/search.py`.
## Follow-on (separate issue, mentioned for context)
Once dispatch is an explicit loop over a registry, running the applicable searches concurrently becomes straightforward — they're read-only, independent, and the results are sorted afterwards, so execution order doesn't matter. That's worth its own discussion though, since it brings in thread-per-connection behaviour under `TestCase`, connection-count limits, and shared lazy state on `SearchInput`. Better to land the registry (and the timing data it enables) first and decide from there.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in snpdb/search.py at @search_receiver around line 840 and SearchInput.search() around line 98; inspect how search_data() is used at variantopedia/views.py:478. Replace the signal-backed dispatch with the proposed registry while leaving the receiver modules and AppConfig.ready() imports unchanged. Done means searches still return the same responses and metadata can be enumerated without dispatching.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, python
- Domain
- backend
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100