diff --git a/papers/commands/list_cmd.py b/papers/commands/list_cmd.py index b0eda6e..306dcd2 100644 --- a/papers/commands/list_cmd.py +++ b/papers/commands/list_cmd.py @@ -1,59 +1,107 @@ -from .. import pretty from .. import repo -from .. import color from . import helpers from ..configs import config from ..uis import get_ui +class InvalidQuery(ValueError): + pass + + def parser(subparsers): parser = subparsers.add_parser('list', help="list papers") parser.add_argument('-k', '--citekeys-only', action='store_true', default=False, dest='citekeys', help='Only returns citekeys of matching papers.') + parser.add_argument('-i', '--ignore-case', action='store_false', + default=None, dest='case_sensitive') + parser.add_argument('-I', '--force-case', action='store_true', + dest='case_sensitive') parser.add_argument('query', nargs='*', help='Paper query (e.g. "year: 2000" or "tags: math")') return parser def command(args): - ui = get_ui() - citekeys = args.citekeys - query = args.query - rp = repo.Repository(config()) - papers = [(n, p) for n, p in enumerate(rp.all_papers()) - if test_paper(query, p)] - ui.print_('\n'.join(helpers.paper_oneliner(p, n = n, citekey_only = citekeys) for n, p in papers)) + papers = filter(lambda p: filter_paper(p, args.query, + case_sensitive=args.case_sensitive), + rp.all_papers()) + ui.print_('\n'.join( + helpers.paper_oneliner(p, n=n, citekey_only=args.citekeys) + for n, p in enumerate(papers))) + + +FIELD_ALIASES = { + 'a': 'author', + 'authors': 'author', + 't': 'title', + 'tags': 'tag', + } + + +def _get_field_value(query_block): + split_block = query_block.split(':') + if len(split_block) != 2: + raise InvalidQuery("Invalid query (%s)" % query_block) + field = split_block[0] + if field in FIELD_ALIASES: + field = FIELD_ALIASES[field] + value = split_block[1] + return (field, value) + + +def _lower(string, lower=True): + if lower: + return string.lower() + else: + return string + + +def _check_author_match(paper, query, case_sensitive=False): + """Only checks within last names.""" + if not 'author' in paper.bibentry.persons: + return False + return any([query in _lower(name, lower=(not case_sensitive)) + for p in paper.bibentry.persons['author'] + for name in p.last()]) + + +def _check_tag_match(paper, query, case_sensitive=False): + return any([query in _lower(t, lower=(not case_sensitive)) + for t in paper.tags]) + + +def _check_field_match(paper, field, query, case_sensitive=False): + return query in _lower(paper.bibentry.fields[field], + lower=(not case_sensitive)) + + +def _check_query_block(paper, query_block, case_sensitive=None): + field, value = _get_field_value(query_block) + if case_sensitive is None: + case_sensitive = not value.islower() + elif not case_sensitive: + value = value.lower() + if field == 'tag': + return _check_tag_match(paper, value, case_sensitive=case_sensitive) + elif field == 'author': + return _check_author_match(paper, value, case_sensitive=case_sensitive) + elif field in paper.bibentry.fields: + return _check_field_match(paper, field, value, + case_sensitive=case_sensitive) + else: + return False -# TODO author is not implemented, should we do it by last name only or more -# complex # TODO implement search by type of document -def test_paper(query_string, p): - for test in query_string: - tmp = test.split(':') - if len(tmp) != 2: - raise ValueError('command not valid') - - field = tmp[0] - value = tmp[1] - - if field in ['tags', 't']: - if value not in p.tags: - return False - elif field in ['author', 'authors', 'a']: # that is the very ugly - if not 'author' in p.bibentry.persons: - return False - a = False - for p in p.bibentry.persons['author']: - if value in p.last()[0]: - a = True - return a - elif field in p.bibentry.fields: - if value not in p.bibentry.fields[field]: - return False - else: - return False - return True +def filter_paper(paper, query, case_sensitive=None): + """If case_sensitive is not given, only check case if query + is not lowercase. + + :args query: list of query blocks (strings) + """ + return all([_check_query_block(paper, query_block, + case_sensitive=case_sensitive) + for query_block in query]) diff --git a/tests/fixtures.py b/tests/fixtures.py index 3098283..23f1413 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,7 +1,6 @@ from pybtex.database import Person -from papers.paper import Paper - +from papers.paper import Paper, get_bibentry_from_string turing1950 = Paper() turing1950.bibentry.fields['title'] = u'Computing machinery and intelligence.' @@ -33,6 +32,8 @@ institution = {Stanford InfoLab}, } """ +page99 = Paper(bibentry=get_bibentry_from_string(pagerankbib)[1]) + pagerankbib_generated = """@techreport{ Page99, author = "Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry", @@ -46,4 +47,4 @@ pagerankbib_generated = """@techreport{ institution = "Stanford InfoLab" } -""" \ No newline at end of file +""" diff --git a/tests/test_queries.py b/tests/test_queries.py new file mode 100644 index 0000000..0b63af2 --- /dev/null +++ b/tests/test_queries.py @@ -0,0 +1,91 @@ +from unittest import TestCase + +import testenv +import fixtures +from papers.commands.list_cmd import (_check_author_match, + _check_field_match, + _check_query_block, + filter_paper, + InvalidQuery) + + +class TestAuthorFilter(TestCase): + + def test_fails_if_no_author(self): + no_doe = fixtures.doe2013.copy() + no_doe.bibentry.persons = {} + self.assertTrue(not _check_author_match(no_doe, 'whatever')) + + def test_match_case(self): + self.assertTrue(_check_author_match(fixtures.doe2013, 'doe')) + self.assertTrue(_check_author_match(fixtures.doe2013, 'doe', + case_sensitive=False)) + + def test_do_not_match_case(self): + self.assertFalse(_check_author_match(fixtures.doe2013, 'dOe')) + self.assertFalse(_check_author_match(fixtures.doe2013, 'doe', + case_sensitive=True)) + + def test_match_not_first_author(self): + self.assertTrue(_check_author_match(fixtures.page99, 'wani')) + + def test_do_not_match_first_name(self): + self.assertTrue(not _check_author_match(fixtures.page99, 'larry')) + + +class TestCheckTag(TestCase): + pass + + +class TestCheckField(TestCase): + + def test_match_case(self): + self.assertTrue(_check_field_match(fixtures.doe2013, 'title', 'nice')) + self.assertTrue(_check_field_match(fixtures.doe2013, 'title', 'nice', + case_sensitive=False)) + self.assertTrue(_check_field_match(fixtures.doe2013, 'year', '2013')) + + def test_do_not_match_case(self): + self.assertFalse(_check_field_match(fixtures.doe2013, 'title', + 'Title', case_sensitive=True)) + self.assertFalse(_check_field_match(fixtures.doe2013, 'title', 'nice', + case_sensitive=True)) + + +class TestCheckQueryBlock(TestCase): + + def test_raise_invalid_if_no_value(self): + with self.assertRaises(InvalidQuery): + _check_query_block(fixtures.doe2013, 'title') + + def test_raise_invalid_if_too_much(self): + with self.assertRaises(InvalidQuery): + _check_query_block(fixtures.doe2013, 'whatever:value:too_much') + + +class TestFilterPaper(TestCase): + + def test_case(self): + self.assertTrue(filter_paper(fixtures.doe2013, ['title:nice'])) + self.assertTrue(filter_paper(fixtures.doe2013, ['title:Nice'])) + self.assertFalse(filter_paper(fixtures.doe2013, ['title:nIce'])) + + def test_fields(self): + self.assertTrue(filter_paper(fixtures.doe2013, ['year:2013'])) + self.assertFalse(filter_paper(fixtures.doe2013, ['year:2014'])) + self.assertTrue(filter_paper(fixtures.doe2013, ['author:doe'])) + self.assertTrue(filter_paper(fixtures.doe2013, ['author:Doe'])) + + def test_tags(self): + self.assertTrue(filter_paper(fixtures.turing1950, ['tag:computer'])) + self.assertFalse(filter_paper(fixtures.turing1950, ['tag:Ai'])) + self.assertTrue(filter_paper(fixtures.turing1950, ['tag:AI'])) + self.assertTrue(filter_paper(fixtures.turing1950, ['tag:ai'])) + + def test_multiple(self): + self.assertTrue(filter_paper(fixtures.doe2013, + ['author:doe', 'year:2013'])) + self.assertFalse(filter_paper(fixtures.doe2013, + ['author:doe', 'year:2014'])) + self.assertFalse(filter_paper(fixtures.doe2013, + ['author:doee', 'year:2014'])) diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 623854a..060665b 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -253,6 +253,35 @@ class TestList(DataCommandTestCase): ] self.execute_cmds(cmds) + def test_list_smart_case(self): + cmds = ['papers init', + 'papers list', + 'papers import data/', + 'papers list title:language author:Saunders', + ] + outs = self.execute_cmds(cmds) + print outs[-1] + self.assertEquals(1, len(outs[-1].split('/n'))) + + def test_list_ignore_case(self): + cmds = ['papers init', + 'papers list', + 'papers import data/', + 'papers list --ignore-case title:lAnguAge author:saunders', + ] + outs = self.execute_cmds(cmds) + self.assertEquals(1, len(outs[-1].split('/n'))) + + def test_list_force_case(self): + cmds = ['papers init', + 'papers list', + 'papers import data/', + 'papers list --force-case title:Language author:saunders', + ] + outs = self.execute_cmds(cmds) + self.assertEquals(0 + 1, len(outs[-1].split('/n'))) + + class TestUsecase(DataCommandTestCase):