From 8a7d1432617c6477f46f1c38cb0342e13c037182 Mon Sep 17 00:00:00 2001 From: Olivier Mangin Date: Wed, 25 Jul 2018 22:20:14 +0200 Subject: [PATCH] Improves behaviors related to bibtex decoding error. - from editor input in add and edit commands, - from files in import command. --- pubs/commands/add_cmd.py | 8 +++++--- pubs/commands/edit_cmd.py | 7 ++++++- pubs/commands/import_cmd.py | 12 +++++++++--- pubs/endecoder.py | 18 ++++++++++++++++-- pubs/uis.py | 3 ++- tests/test_apis.py | 4 ++-- tests/test_endecoder.py | 5 +++++ tests/test_usecase.py | 37 ++++++++++++++++++++++++++++++++++++- 8 files changed, 81 insertions(+), 13 deletions(-) diff --git a/pubs/commands/add_cmd.py b/pubs/commands/add_cmd.py index a22c0a5..d193b5e 100644 --- a/pubs/commands/add_cmd.py +++ b/pubs/commands/add_cmd.py @@ -11,6 +11,7 @@ from .. import templates from .. import apis from .. import pretty from .. import utils +from .. import endecoder from ..completion import CommaSeparatedTagsCompletion @@ -57,12 +58,13 @@ def bibentry_from_editor(conf, ui, rp): bibstruct.verify_bibdata(bibentry) # REFACTOR Generate citykey again = False - except ValueError: + + except endecoder.EnDecoder.BibDecodingError: again = ui.input_yn( - question='Invalid bibfile. Edit again ?', + question='Invalid bibfile. Edit again?', default='y') if not again: - ui.exit(0) + ui.exit() return bibentry diff --git a/pubs/commands/edit_cmd.py b/pubs/commands/edit_cmd.py index b09c885..8cc175b 100644 --- a/pubs/commands/edit_cmd.py +++ b/pubs/commands/edit_cmd.py @@ -64,9 +64,14 @@ def command(conf, args): 'as `{}`.'.format(citekey, new_paper.citekey))) else: ui.info(('Paper `{}` was successfully edited.'.format( - citekey))) + citekey))) break + except coder.BibDecodingError: + if not ui.input_yn(question="Error parsing bibdata. Edit again?"): + ui.error("Aborting, paper not updated.") + ui.exit() + except repo.CiteKeyCollision: options = ['overwrite', 'edit again', 'abort'] choice = options[ui.input_choice( diff --git a/pubs/commands/import_cmd.py b/pubs/commands/import_cmd.py index 9ccb935..05279b2 100644 --- a/pubs/commands/import_cmd.py +++ b/pubs/commands/import_cmd.py @@ -27,7 +27,7 @@ def parser(subparsers, conf): return parser -def many_from_path(bibpath): +def many_from_path(ui, bibpath): """Extract list of papers found in bibliographic files in path. The behavior is to: @@ -49,11 +49,17 @@ def many_from_path(bibpath): biblist = [] for filepath in all_files: - biblist.append(coder.decode_bibdata(read_text_file(filepath))) + try: + biblist.append(coder.decode_bibdata(read_text_file(filepath))) + except coder.BibDecodingError: + ui.error("Could not parse bibtex at {}. Aborting import.".format(filepath)) + ui.exit() papers = {} for b in biblist: for k, b in b.items(): + if k in papers: + ui.warning('Duplicated citekey {}. Keeping last.'.format(k)) try: papers[k] = Paper(k, b) papers[k].added = datetime.datetime.now() @@ -75,7 +81,7 @@ def command(conf, args): rp = repo.Repository(conf) # Extract papers from bib - papers = many_from_path(bibpath) + papers = many_from_path(ui, bibpath) keys = args.keys or papers.keys() for k in keys: p = papers[k] diff --git a/pubs/endecoder.py b/pubs/endecoder.py index 8fd6941..259b8d9 100644 --- a/pubs/endecoder.py +++ b/pubs/endecoder.py @@ -66,6 +66,16 @@ class EnDecoder(object): * encode_bibdata will try to recognize exceptions """ + class BibDecodingError(Exception): + + message = "Could not parse provided bibdata:\n---\n{}\n---" + + def __init__(self, bibdata): + self.data = bibdata + + def __str__(self): + return self.message.format(self.data) + bwriter = bp.bwriter.BibTexWriter() bwriter.display_order = BIBFIELD_ORDER @@ -103,7 +113,10 @@ class EnDecoder(object): return entry def decode_bibdata(self, bibdata): - """""" + """Decodes bibdata from string. + + If the decoding fails, returns a BibParseError. + """ try: entries = bp.bparser.BibTexParser( bibdata, common_strings=True, @@ -121,4 +134,5 @@ class EnDecoder(object): except Exception: import traceback traceback.print_exc() - raise ValueError('could not parse provided bibdata:\n{}'.format(bibdata)) + raise self.BibDecodingError(bibdata) + # TODO: filter exceptions from pyparsing and pass reason upstream diff --git a/pubs/uis.py b/pubs/uis.py index d5ddd88..3c9aac6 100644 --- a/pubs/uis.py +++ b/pubs/uis.py @@ -104,6 +104,7 @@ class PrintUI(object): self.exit() return True # never happens + class InputUI(PrintUI): """UI class. Stores configuration parameters and system information. """ @@ -118,7 +119,7 @@ class InputUI(PrintUI): except EOFError: self.error('Standard input ended while waiting for answer.') self.exit(1) - return ustr(data) #.decode('utf-8') + return ustr(data) #.decode('utf-8') def input_choice_ng(self, options, option_chars=None, default=None, question=''): """Ask the user to chose between a set of options. The user is asked diff --git a/tests/test_apis.py b/tests/test_apis.py index 087f32f..a83481a 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -32,7 +32,7 @@ class TestDOI2Bibtex(unittest.TestCase): def test_parse_fails_on_incorrect_DOI(self): bib = doi2bibtex('999999') - with self.assertRaises(ValueError): + with self.assertRaises(EnDecoder.BibDecodingError): self.endecoder.decode_bibdata(bib) @@ -56,7 +56,7 @@ class TestISBN2Bibtex(unittest.TestCase): def test_parse_fails_on_incorrect_ISBN(self): bib = doi2bibtex('9' * 13) - with self.assertRaises(ValueError): + with self.assertRaises(EnDecoder.BibDecodingError): self.endecoder.decode_bibdata(bib) diff --git a/tests/test_endecoder.py b/tests/test_endecoder.py index ab8dc07..a6e3043 100644 --- a/tests/test_endecoder.py +++ b/tests/test_endecoder.py @@ -147,6 +147,11 @@ class TestEnDecode(unittest.TestCase): self.assertIn('author', entry1) self.assertIn('institution', entry1) + def test_endecodes_raises_exception(self): + decoder = endecoder.EnDecoder() + with self.assertRaises(decoder.BibDecodingError): + decoder.decode_bibdata("@misc{I am not a correct bibtex{{}") + if __name__ == '__main__': unittest.main() diff --git a/tests/test_usecase.py b/tests/test_usecase.py index bdc1de5..d60bdb2 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -337,11 +337,20 @@ class TestAdd(URLContentTestCase): def test_add_no_citekey_fails(self): # See #113 cmds = ['pubs init', - ('pubs add', [str_fixtures.bibtex_no_citekey]), + ('pubs add', [str_fixtures.bibtex_no_citekey, 'n']), ] with self.assertRaises(FakeSystemExit): self.execute_cmds(cmds) + def test_add_edit_fails(self): + cmds = ['pubs init', + ('pubs add', + ['@misc{I am not a correct bibtex{{}', 'n']), + ] + with self.assertRaises(FakeSystemExit) as cm: + self.execute_cmds(cmds) + self.assertEqual(cm.exception.code, 1) + class TestList(DataCommandTestCase): @@ -690,6 +699,32 @@ class TestUsecase(DataCommandTestCase): ] self.execute_cmds(cmds) + def test_editor_succeeds_on_second_edit(self): + cmds = ['pubs init', + 'pubs add data/pagerank.bib', + ('pubs edit Page99', [ + '', 'y', + '@misc{Page99, title="TTT", author="X. YY"}', '']), + ('pubs list', [], '[Page99] YY, X. "TTT" \n') + ] + self.execute_cmds(cmds) + + def test_add_aborts(self): + with self.assertRaises(FakeSystemExit): + cmds = ['pubs init', + ('pubs add New', ['']), + ] + self.execute_cmds(cmds) + + def test_add_succeeds_on_second_edit(self): + cmds = ['pubs init', + ('pubs add', [ + '', 'y', + '@misc{New, title="TTT", author="X. YY"}', '']), + ('pubs list', [], '[New] YY, X. "TTT" \n') + ] + self.execute_cmds(cmds) + def test_editor_success(self): cmds = ['pubs init', ('pubs add', [str_fixtures.bibtex_external0]),