Skip to content

Commit

Permalink
Search entry source support. #276
Browse files Browse the repository at this point in the history
  • Loading branch information
lemon24 committed Nov 28, 2024
1 parent 34ac5f1 commit 341f26f
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 4 deletions.
21 changes: 18 additions & 3 deletions src/reader/_storage/_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .._types import Change
from .._types import EntryFilter
from .._utils import exactly_one
from .._utils import resolve_path
from .._utils import zero_or_one
from ..exceptions import ChangeTrackingNotEnabledError
from ..exceptions import EntryNotFoundError
Expand Down Expand Up @@ -64,6 +65,13 @@
# 255 letters / 4.7 letters per word (average in English)
TOKENS = 54

FEED_TITLE_PATHS = [
'.feed.title',
'.feed.user_title',
'.source.title',
'.feed_resolved_title',
]


class Search:
"""Search provider tightly coupled to the SQLite storage.
Expand Down Expand Up @@ -337,8 +345,15 @@ def _insert_into_search_one_chunk(self, changes: list[Change]) -> None:
final.append((None, None))

stripped_title = self.strip_html(entry.title)
feed_title = entry.feed.user_title or entry.feed.title
is_feed_user_title = bool(entry.feed.user_title)

feed_resolved_title = entry.feed_resolved_title
for is_feed_user_title, path in enumerate(FEED_TITLE_PATHS): # noqa: B007
if feed_title := resolve_path(entry, path):
if feed_title == feed_resolved_title:
break
else:
feed_title = None
is_feed_user_title = 0
stripped_feed_title = self.strip_html(feed_title)

stripped[change] = [
Expand Down Expand Up @@ -589,7 +604,7 @@ def entry_search_result_factory(
if title:
metadata['.title'] = extract(title)
if feed_title:
path = '.feed.title' if not is_feed_user_title else '.feed.user_title'
path = FEED_TITLE_PATHS[is_feed_user_title]
metadata[path] = extract(feed_title)

rv_content = {c['path']: extract(c['value']) for c in content if c['path']}
Expand Down
7 changes: 7 additions & 0 deletions src/reader/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,10 @@ def __getattr__(name: str) -> Any:
return rv

return __getattr__


def resolve_path(o: object, path: str) -> Any | None:
try:
return eval('o' + path, {'o': o})
except AttributeError:
return None
29 changes: 28 additions & 1 deletion src/reader/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@ def resource_id(self) -> tuple[str]:
"""
return (self.url,)

@property
def resolved_title(self) -> str | None:
""":attr:`user_title` or :attr:`title`.
.. versionadded:: 3.16
"""
return self.user_title or self.title


@dataclass(frozen=True)
class ExceptionInfo(_namedtuple_compat):
Expand Down Expand Up @@ -372,6 +381,23 @@ def get_content(self, *, prefer_summary: bool = False) -> Content | None:
"""
return _get_entry_content(self, prefer_summary)

@property
def feed_resolved_title(self) -> str | None:
"""Feed :attr:`~Feed.resolved_title`, source :attr:`~EntrySource.title`,
or ``"{source} ({feed})"`` if both are present.
.. versionadded:: 3.16
"""
title = self.feed.resolved_title
source = self.source
source_title = source.title if source else None
if not source_title:
return title
if not title:
return source_title
return f"{source_title} ({title})"


@dataclass(frozen=True)
class Content(_namedtuple_compat):
Expand Down Expand Up @@ -654,7 +680,8 @@ class EntrySearchResult(_namedtuple_compat):
id: str

#: Matching entry metadata, in arbitrary order.
#: Currently entry.title and entry.feed.user_title/.title.
#: Currently entry.title and entry.feed.user_title/.title /
#: entry.source.title / entry.feed_resolved_title.
metadata: Mapping[str, HighlightedString] = field(
default_factory=lambda: MappingProxyType({}),
)
Expand Down
48 changes: 48 additions & 0 deletions tests/test_reader_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from reader import Enclosure
from reader import EntrySearchCounts
from reader import EntrySearchResult
from reader import EntrySource
from reader import FeedNotFoundError
from reader import HighlightedString
from reader import Reader
Expand Down Expand Up @@ -967,3 +968,50 @@ def test_add_entry_basic(reader):
assert result.resource_id == ('1', '1')
assert result.metadata['.title'].apply('*', '*') == 'my entry'
assert result.content['.summary'].apply('*', '*') == 'I am a *summary*'


def test_feed_resolved_title(reader):
reader._parser = parser = Parser()

def add_feed(url, title=None, user_title=None, source_title=None):
feed = parser.feed(url, title=title)
source = EntrySource(title=source_title) if source_title else None
parser.entry(int(feed.url), '1', title=None, source=source)
reader.add_feed(feed)
reader.set_feed_user_title(feed, user_title)

add_feed(0, title='no match')
add_feed(1)
add_feed(2, title='title')
add_feed(3, title='title', user_title='user title')
add_feed(4, title='no match', source_title='source title')
add_feed(5, source_title='source title')
add_feed(6, title='title', source_title='source title')
add_feed(7, title='title', user_title='user title', source_title='source title')

reader.update_feeds()
reader.update_search()

assert {
e.feed_url: {k: v.apply('>', '<') for k, v in e.metadata.items()}
for e in reader.search_entries('title')
} == {
'2': {
'.feed.title': '>title<',
},
'3': {
'.feed.user_title': 'user >title<',
},
'4': {
'.feed_resolved_title': 'source >title< (no match)',
},
'5': {
'.source.title': 'source >title<',
},
'6': {
'.feed_resolved_title': 'source >title< (>title<)',
},
'7': {
'.feed_resolved_title': 'source >title< (user >title<)',
},
}

0 comments on commit 341f26f

Please sign in to comment.