Merge pull request #149 from pubs/fix87

Fix #87 (more robust error handling and list command)
main
Olivier Mangin 7 years ago committed by GitHub
commit c6edacf3ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -29,6 +29,8 @@
### Fixed bugs ### Fixed bugs
- [[#149]](https://github.com/pubs/pubs/issues/149) More robust handling of parsing and citekey errors [(#87)](https://github.com/pubs/pubs/pull/87)
- [[#148]](https://github.com/pubs/pubs/issues/148) Fix compatibility with Pyfakefs 3.7 [(#151)](https://github.com/pubs/pubs/pull/151) - [[#148]](https://github.com/pubs/pubs/issues/148) Fix compatibility with Pyfakefs 3.7 [(#151)](https://github.com/pubs/pubs/pull/151)
- [[#95]](https://github.com/pubs/pubs/issues/95) Error message when editor is missing [(#141)](https://github.com/pubs/pubs/issues/141) - [[#95]](https://github.com/pubs/pubs/issues/95) Error message when editor is missing [(#141)](https://github.com/pubs/pubs/issues/141)

@ -11,6 +11,7 @@ from .. import templates
from .. import apis from .. import apis
from .. import pretty from .. import pretty
from .. import utils from .. import utils
from .. import endecoder
from ..completion import CommaSeparatedTagsCompletion from ..completion import CommaSeparatedTagsCompletion
@ -57,12 +58,13 @@ def bibentry_from_editor(conf, ui, rp):
bibstruct.verify_bibdata(bibentry) bibstruct.verify_bibdata(bibentry)
# REFACTOR Generate citykey # REFACTOR Generate citykey
again = False again = False
except ValueError:
except endecoder.EnDecoder.BibDecodingError:
again = ui.input_yn( again = ui.input_yn(
question='Invalid bibfile. Edit again ?', question='Invalid bibfile. Edit again?',
default='y') default='y')
if not again: if not again:
ui.exit(0) ui.exit()
return bibentry return bibentry

@ -64,9 +64,14 @@ def command(conf, args):
'as `{}`.'.format(citekey, new_paper.citekey))) 'as `{}`.'.format(citekey, new_paper.citekey)))
else: else:
ui.info(('Paper `{}` was successfully edited.'.format( ui.info(('Paper `{}` was successfully edited.'.format(
citekey))) citekey)))
break 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: except repo.CiteKeyCollision:
options = ['overwrite', 'edit again', 'abort'] options = ['overwrite', 'edit again', 'abort']
choice = options[ui.input_choice( choice = options[ui.input_choice(

@ -13,6 +13,10 @@ from ..uis import get_ui
from ..content import system_path, read_text_file from ..content import system_path, read_text_file
_ABORT_USE_IGNORE_MSG = "Aborting import. Use --ignore-malformed to ignore."
_IGNORING_MSG = " Ignoring it."
def parser(subparsers, conf): def parser(subparsers, conf):
parser = subparsers.add_parser('import', parser = subparsers.add_parser('import',
help='import paper(s) to the repository') help='import paper(s) to the repository')
@ -24,10 +28,12 @@ def parser(subparsers, conf):
help="one or several keys to import from the file") help="one or several keys to import from the file")
parser.add_argument('-O', '--overwrite', action='store_true', default=False, parser.add_argument('-O', '--overwrite', action='store_true', default=False,
help="Overwrite keys already in the database") help="Overwrite keys already in the database")
parser.add_argument('-i', '--ignore-malformed', action='store_true', default=False,
help="Ignore malformed and unreadable files and entries")
return parser return parser
def many_from_path(bibpath): def many_from_path(ui, bibpath, ignore=False):
"""Extract list of papers found in bibliographic files in path. """Extract list of papers found in bibliographic files in path.
The behavior is to: The behavior is to:
@ -49,16 +55,31 @@ def many_from_path(bibpath):
biblist = [] biblist = []
for filepath in all_files: 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:
error = "Could not parse bibtex at {}.".format(filepath)
if ignore:
ui.warning(error + _IGNORING_MSG)
else:
ui.error(error + _ABORT_USE_IGNORE_MSG)
ui.exit()
papers = {} papers = {}
for b in biblist: for b in biblist:
for k, b in b.items(): for k, b in b.items():
if k in papers:
ui.warning('Duplicated citekey {}. Keeping the last one.'.format(k))
try: try:
papers[k] = Paper(k, b) papers[k] = Paper(k, b)
papers[k].added = datetime.datetime.now() papers[k].added = datetime.datetime.now()
except ValueError as e: except ValueError as e:
papers[k] = e error = 'Could not load entry for citekey {} ({}).'.format(k, e)
if ignore:
ui.warning(error + _IGNORING_MSG)
else:
ui.error(error + _ABORT_USE_IGNORE_MSG)
ui.exit()
return papers return papers
@ -75,20 +96,17 @@ def command(conf, args):
rp = repo.Repository(conf) rp = repo.Repository(conf)
# Extract papers from bib # Extract papers from bib
papers = many_from_path(bibpath) papers = many_from_path(ui, bibpath, ignore=args.ignore_malformed)
keys = args.keys or papers.keys() keys = args.keys or papers.keys()
for k in keys: for k in keys:
p = papers[k] p = papers[k]
if isinstance(p, Exception): rp.push_paper(p, overwrite=args.overwrite)
ui.error('Could not load entry for citekey {}.'.format(k)) ui.info('{} imported.'.format(color.dye_out(p.citekey, 'citekey')))
docfile = bibstruct.extract_docfile(p.bibdata)
if docfile is None:
ui.warning("No file for {}.".format(p.citekey))
else: else:
rp.push_paper(p, overwrite=args.overwrite) rp.push_doc(p.citekey, docfile, copy=copy)
ui.info('{} imported.'.format(color.dye_out(p.citekey, 'citekey'))) # FIXME should move the file if configured to do so.
docfile = bibstruct.extract_docfile(p.bibdata)
if docfile is None:
ui.warning("No file for {}.".format(p.citekey))
else:
rp.push_doc(p.citekey, docfile, copy=copy)
#FIXME should move the file if configured to do so.
rp.close() rp.close()

@ -45,7 +45,11 @@ class DataBroker(object):
def pull_bibentry(self, citekey): def pull_bibentry(self, citekey):
bibdata_raw = self.filebroker.pull_bibfile(citekey) bibdata_raw = self.filebroker.pull_bibfile(citekey)
return self.endecoder.decode_bibdata(bibdata_raw) try:
return self.endecoder.decode_bibdata(bibdata_raw)
except self.endecoder.BibDecodingError as e:
e.message = "Unable to decode bibtex for paper {}.".format(citekey)
raise e
def push_metadata(self, citekey, metadata): def push_metadata(self, citekey, metadata):
metadata_raw = self.endecoder.encode_metadata(metadata) metadata_raw = self.endecoder.encode_metadata(metadata)

@ -66,6 +66,16 @@ class EnDecoder(object):
* encode_bibdata will try to recognize exceptions * 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 = bp.bwriter.BibTexWriter()
bwriter.display_order = BIBFIELD_ORDER bwriter.display_order = BIBFIELD_ORDER
@ -103,7 +113,10 @@ class EnDecoder(object):
return entry return entry
def decode_bibdata(self, bibdata): def decode_bibdata(self, bibdata):
"""""" """Decodes bibdata from string.
If the decoding fails, returns a BibParseError.
"""
try: try:
entries = bp.bparser.BibTexParser( entries = bp.bparser.BibTexParser(
bibdata, common_strings=True, bibdata, common_strings=True,
@ -121,4 +134,5 @@ class EnDecoder(object):
except Exception: except Exception:
import traceback import traceback
traceback.print_exc() 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

@ -119,7 +119,7 @@ class InputUI(PrintUI):
except EOFError: except EOFError:
self.error('Standard input ended while waiting for answer.') self.error('Standard input ended while waiting for answer.')
self.exit(1) 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=''): 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 """Ask the user to chose between a set of options. The user is asked

@ -77,6 +77,16 @@ class FakeInput():
if md.__name__ == 'pubs.uis': if md.__name__ == 'pubs.uis':
md.InputUI.editor_input = self md.InputUI.editor_input = self
md.InputUI.edit_file = self.input_to_file md.InputUI.edit_file = self.input_to_file
# Do not catch UnexpectedInput
original_handler = md.InputUI.handle_exception
def handler(ui, exc):
if isinstance(exc, self.UnexpectedInput):
raise
else:
original_handler(ui, exc)
md.InputUI.handle_exception = handler
def input_to_file(self, path_to_file, temporary=True): def input_to_file(self, path_to_file, temporary=True):
content.write_file(path_to_file, self()) content.write_file(path_to_file, self())

@ -69,7 +69,7 @@ bibtex_no_citekey = """@Manual{,
} }
""" """
bibtex_month= """@inproceedings{Goyal2017, bibtex_month = """@inproceedings{Goyal2017,
author = {Goyal, Anirudh and Sordoni, Alessandro and C{\^{o}}t{\'{e}}, Marc-Alexandre and Ke, Nan Rosemary and Bengio, Yoshua}, author = {Goyal, Anirudh and Sordoni, Alessandro and C{\^{o}}t{\'{e}}, Marc-Alexandre and Ke, Nan Rosemary and Bengio, Yoshua},
title = {Z-Forcing: Training Stochastic Recurrent Networks}, title = {Z-Forcing: Training Stochastic Recurrent Networks},
year = {2017}, year = {2017},
@ -78,6 +78,12 @@ bibtex_month= """@inproceedings{Goyal2017,
} }
""" """
not_bibtex = """@misc{this looks,
like = a = bibtex file but
, is not a real one!
"""
sample_conf = """ sample_conf = """
[main] [main]

@ -32,7 +32,7 @@ class TestDOI2Bibtex(unittest.TestCase):
def test_parse_fails_on_incorrect_DOI(self): def test_parse_fails_on_incorrect_DOI(self):
bib = doi2bibtex('999999') bib = doi2bibtex('999999')
with self.assertRaises(ValueError): with self.assertRaises(EnDecoder.BibDecodingError):
self.endecoder.decode_bibdata(bib) self.endecoder.decode_bibdata(bib)
@ -56,7 +56,7 @@ class TestISBN2Bibtex(unittest.TestCase):
def test_parse_fails_on_incorrect_ISBN(self): def test_parse_fails_on_incorrect_ISBN(self):
bib = doi2bibtex('9' * 13) bib = doi2bibtex('9' * 13)
with self.assertRaises(ValueError): with self.assertRaises(EnDecoder.BibDecodingError):
self.endecoder.decode_bibdata(bib) self.endecoder.decode_bibdata(bib)

@ -52,7 +52,7 @@ class TestEnDecode(unittest.TestCase):
self.assertEqual(bibraw1, bibraw2) self.assertEqual(bibraw1, bibraw2)
def test_endecode_bibtex(self): def test_endecode_bibtex_converts_month_string(self):
"""Test if `month=dec` is correctly recognized and transformed into """Test if `month=dec` is correctly recognized and transformed into
`month={December}`""" `month={December}`"""
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()
@ -147,6 +147,11 @@ class TestEnDecode(unittest.TestCase):
self.assertIn('author', entry1) self.assertIn('author', entry1)
self.assertIn('institution', 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__': if __name__ == '__main__':
unittest.main() unittest.main()

@ -128,7 +128,7 @@ class CommandTestCase(fake_env.TestFakeFs):
outs.append(color.undye(actual_out)) outs.append(color.undye(actual_out))
else: else:
pubs_cmd.execute(actual_cmd.split()) pubs_cmd.execute(actual_cmd.split())
except fake_env.FakeInput.UnexpectedInput as e: except fake_env.FakeInput.UnexpectedInput:
self.fail('Unexpected input asked by command: {}.'.format( self.fail('Unexpected input asked by command: {}.'.format(
actual_cmd)) actual_cmd))
return outs return outs
@ -197,7 +197,7 @@ class TestAlone(CommandTestCase):
def test_alone_misses_command(self): def test_alone_misses_command(self):
with self.assertRaises(FakeSystemExit) as cm: with self.assertRaises(FakeSystemExit) as cm:
self.execute_cmds(['pubs']) self.execute_cmds(['pubs'])
self.assertEqual(cm.exception.code, 2) self.assertEqual(cm.exception.code, 2)
def test_alone_prints_help(self): def test_alone_prints_help(self):
# capturing the output of `pubs --help` is difficult because argparse # capturing the output of `pubs --help` is difficult because argparse
@ -334,11 +334,20 @@ class TestAdd(URLContentTestCase):
def test_add_no_citekey_fails(self): def test_add_no_citekey_fails(self):
# See #113 # See #113
cmds = ['pubs init', cmds = ['pubs init',
('pubs add', [str_fixtures.bibtex_no_citekey]), ('pubs add', [str_fixtures.bibtex_no_citekey, 'n']),
] ]
with self.assertRaises(FakeSystemExit): with self.assertRaises(FakeSystemExit):
self.execute_cmds(cmds) 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): class TestList(DataCommandTestCase):
@ -679,16 +688,40 @@ class TestUsecase(DataCommandTestCase):
self.assertFileContentEqual(os.path.expanduser('~/.pubsrc'), self.assertFileContentEqual(os.path.expanduser('~/.pubsrc'),
str_fixtures.sample_conf) str_fixtures.sample_conf)
def test_editor_abort(self): def test_editor_aborts(self):
with self.assertRaises(FakeSystemExit): with self.assertRaises(FakeSystemExit):
cmds = ['pubs init', cmds = ['pubs init',
('pubs add', ['abc', 'n']),
('pubs add', ['abc', 'y', 'abc', 'n']),
'pubs add data/pagerank.bib', 'pubs add data/pagerank.bib',
('pubs edit Page99', ['', 'a']), ('pubs edit Page99', ['', 'n']),
]
self.execute_cmds(cmds)
def test_editor_succeeds_on_second_edit(self):
cmds = ['pubs init',
'pubs add data/pagerank.bib',
('pubs edit Page99', [
'@misc{Page99, title="TTT" author="X. YY"}', '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) 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): def test_editor_success(self):
cmds = ['pubs init', cmds = ['pubs init',
('pubs add', [str_fixtures.bibtex_external0]), ('pubs add', [str_fixtures.bibtex_external0]),
@ -790,6 +823,25 @@ class TestUsecase(DataCommandTestCase):
outs = self.execute_cmds(cmds) outs = self.execute_cmds(cmds)
self.assertEqual(1 + 1, len(outs[-1].split('\n'))) self.assertEqual(1 + 1, len(outs[-1].split('\n')))
def test_import_fails_without_ignore(self):
with FakeFileOpen(self.fs)('data/fake.bib', 'w') as f:
f.write(str_fixtures.not_bibtex)
cmds = ['pubs init',
'pubs import data/ Page99',
]
with self.assertRaises(FakeSystemExit):
self.execute_cmds(cmds)
def test_import_ignores(self):
with FakeFileOpen(self.fs)('data/fake.bib', 'w') as f:
f.write(str_fixtures.not_bibtex)
cmds = ['pubs init',
'pubs import --ignore-malformed data/ Page99',
'pubs list'
]
outs = self.execute_cmds(cmds)
self.assertEqual(1 + 1, len(outs[-1].split('\n')))
def test_update(self): def test_update(self):
cmds = ['pubs init', cmds = ['pubs init',
'pubs add data/pagerank.bib', 'pubs add data/pagerank.bib',

Loading…
Cancel
Save