diff --git a/conservancy_beancount/filters.py b/conservancy_beancount/filters.py index 8cd92c6..5bfba62 100644 --- a/conservancy_beancount/filters.py +++ b/conservancy_beancount/filters.py @@ -17,6 +17,7 @@ import re from . import data +from . import rtutil from typing import ( Iterable, @@ -53,5 +54,5 @@ def filter_for_rt_id(postings: Postings, ticket_id: Union[int, str]) -> Postings This functions yields postings where the *first* rt-id matches the given ticket number. """ - regexp = r'^\s*rt:(?://ticket/)?{}\b'.format(re.escape(str(ticket_id))) + regexp = rtutil.RT.metadata_regexp(ticket_id, first_link_only=True) return filter_meta_match(postings, 'rt-id', regexp) diff --git a/conservancy_beancount/rtutil.py b/conservancy_beancount/rtutil.py index a0173ba..f8c7974 100644 --- a/conservancy_beancount/rtutil.py +++ b/conservancy_beancount/rtutil.py @@ -266,6 +266,35 @@ class RT: def exists(self, ticket_id: RTId, attachment_id: Optional[RTId]=None) -> bool: return self.url(ticket_id, attachment_id) is not None + @classmethod + def metadata_regexp(self, + ticket_id: RTId, + attachment_id: Optional[RTId]=None, + *, + first_link_only: bool=False + ) -> str: + """Return a pattern to find RT links in metadata + + Given a ticket ID and optional attachment ID, this method returns a + regular expression pattern that will find matching RT links in a + metadata value string, written in any format. + + If the keyword-only argument first_link_only is true, the pattern will + only match the first link in a metadata string. Otherwise the pattern + matches any link in the string (the default). + """ + if first_link_only: + prolog = r'^\s*' + else: + prolog = r'(?:^|\s)' + if attachment_id is None: + attachment = '' + else: + attachment = r'/(?:attachments?/)?{}'.format(attachment_id) + ticket = r'rt:(?://ticket/)?{}'.format(ticket_id) + epilog = r'/?(?:$|\s)' + return f'{prolog}{ticket}{attachment}{epilog}' + @classmethod def parse(cls, s: str) -> Optional[Tuple[str, Optional[str]]]: for regexp in cls.PARSE_REGEXPS: diff --git a/tests/test_filters.py b/tests/test_filters.py index 1c2cbc2..24b02c4 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -111,11 +111,9 @@ def test_filter_for_rt_id(cc_txn_pair, ticket_id, expected_indexes): @pytest.mark.parametrize('rt_id', [ 'rt:450/', - 'rt:450/678', ' rt:450 rt:540', 'rt://ticket/450', 'rt://ticket/450/', - 'rt://ticket/450/678', ' rt://ticket/450', 'rt://ticket/450 rt://ticket/540', ]) diff --git a/tests/test_rtutil.py b/tests/test_rtutil.py index c57bf40..1fed60f 100644 --- a/tests/test_rtutil.py +++ b/tests/test_rtutil.py @@ -15,6 +15,8 @@ # along with this program. If not, see . import contextlib +import itertools +import re import pytest @@ -62,6 +64,28 @@ def test_url(rt, ticket_id, attachment_id, expected): expected = DEFAULT_RT_URL + expected assert rt.url(ticket_id, attachment_id) == expected +@pytest.mark.parametrize('attachment_id,first_link_only', itertools.product( + [245, None], + [True, False], +)) +def test_metadata_regexp(rt, attachment_id, first_link_only): + if attachment_id is None: + match_links = ['rt:220', 'rt://ticket/220'] + else: + match_links = [f'rt:220/{attachment_id}', + f'rt://ticket/220/attachments/{attachment_id}'] + regexp = rt.metadata_regexp(220, attachment_id, first_link_only=first_link_only) + for link in match_links: + assert re.search(regexp, link) + assert re.search(regexp, link + ' link2') + assert re.search(regexp, link + '0') is None + assert re.search(regexp, 'a' + link) is None + end_match = re.search(regexp, 'link0 ' + link) + if first_link_only: + assert end_match is None + else: + assert end_match + @pytest.mark.parametrize('attachment_id', [ 13, None,