diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..314ccbd --- /dev/null +++ b/changelog.md @@ -0,0 +1,72 @@ +# Changelog + + +## Current master + +[Full Changelog](https://github.com/pubs/pubs/compare/v0.7.0...master) + + +### Implemented enhancements + +- Better dialog after editing paper [(#142)](https://github.com/pubs/pubs/issues/142) + +- Add a command to open urls ([#139](https://github.com/pubs/pubs/issues/139) by [ksunden](https://github.com/ksunden)) + +- More robust cache on version change [(#138)](https://github.com/pubs/pubs/issues/138) + +- Allow utf8 citekeys [(#133)](https://github.com/pubs/pubs/issues/133) + +- Adds tag list completion in `pubs add -t ` [(#130)](https://github.com/pubs/pubs/issues/130) + +- Wider Travis coverage ([#107](https://github.com/pubs/pubs/issues/107) and [#108](https://github.com/pubs/pubs/issues/108)) + +- Uses bibtexparser bwriter instead of internal encoder and adds `--ignore-fields` option to export. [(#106)](https://github.com/pubs/pubs/issues/106) + +- Configurable alias descriptions ([#104](https://github.com/pubs/pubs/issues/104) by [wflynny](https://github.com/wflynny)) + +- Support year ranges in query [(#102)](https://github.com/pubs/pubs/issues/102) + + +### 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) + +- [[#95]](https://github.com/pubs/pubs/issues/95) Error message when editor is missing [(#141)](https://github.com/pubs/pubs/issues/141) + +- Fixes tests for printing help on `--help` and without argument. [(#137)](https://github.com/pubs/pubs/issues/137) + +- [[#126]](https://github.com/pubs/pubs/issues/126) Removes journal customization [(#127)](https://github.com/pubs/pubs/issues/127) + +- Fixes Travis failure on installing python3 for OSX [(#125)](https://github.com/pubs/pubs/issues/125) + +- [[#119]](https://github.com/pubs/pubs/issues/119) Removes link and DOI customization. [(#124)](https://github.com/pubs/pubs/issues/124) + +- [[#122]](https://github.com/pubs/pubs/issues/122) Fixes common strings [(#123)](https://github.com/pubs/pubs/issues/123) + +- [[#28]](https://github.com/pubs/pubs/issues/28) allow utf8 in citekeys [(#120)](https://github.com/pubs/pubs/issues/120) + +- Fixes field orders to use 'url' and fixes broken test. [(#118)](https://github.com/pubs/pubs/issues/118) + +- [[#25]](https://github.com/pubs/pubs/issues/25) Fix bibtex testcase [(#117)](https://github.com/pubs/pubs/issues/117) + +- [[#103]](https://github.com/pubs/pubs/issues/103) Fixes unicode comparison [(#116)](https://github.com/pubs/pubs/issues/116) + +- [[#95]](https://github.com/pubs/pubs/issues/95) robust handling of DOIs ([#105](https://github.com/pubs/pubs/issues/105) by [wflynny](https://github.com/wflynny)) + +- [[#99]](https://github.com/pubs/pubs/issues/99) Print help when no subcommand is provided ([#100](https://github.com/pubs/pubs/issues/100) by [wflynny](https://github.com/wflynny)) + +- Fix defaults not used in config. [(#97)](https://github.com/pubs/pubs/issues/97) + +- Fixes content not read from urls because of call to `os.abspath` [(#96)](https://github.com/pubs/pubs/issues/96) + +- [[#93]](https://github.com/pubs/pubs/issues/93) actually save the modifications on `edit -m`. [(#94)](https://github.com/pubs/pubs/issues/94) + +- [[#88]](https://github.com/pubs/pubs/issues/88) Adds proper escaping for +arguments in alias plugin. [(#91)](https://github.com/pubs/pubs/issues/91) + + +## [v0.7.0](https://github.com/pubs/pubs/compare/v0.6.0...v0.7.0) (2017-08-06) + +[Full Changelog](https://github.com/pubs/pubs/compare/v0.6.0...v0.7.0) diff --git a/pubs/commands/add_cmd.py b/pubs/commands/add_cmd.py index 4930b6b..e3632f4 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 @@ -58,12 +59,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 11d7bfc..8cc175b 100644 --- a/pubs/commands/edit_cmd.py +++ b/pubs/commands/edit_cmd.py @@ -54,12 +54,24 @@ def command(conf, args): new_paper = Paper(paper.citekey, paper.bibdata, metadata=content) rp.push_paper(new_paper, overwrite=True, event=False) + ui.info(('The metadata of paper `{}` was successfully ' + 'edited.'.format(citekey))) else: new_paper = Paper.from_bibentry(content, metadata=paper.metadata) - rp.rename_paper(new_paper, old_citekey=paper.citekey) + if rp.rename_paper(new_paper, old_citekey=paper.citekey): + ui.info(('Paper `{}` was successfully edited and renamed ' + 'as `{}`.'.format(citekey, new_paper.citekey))) + else: + ui.info(('Paper `{}` was successfully edited.'.format( + 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( @@ -71,6 +83,7 @@ def command(conf, args): break elif choice == 'overwrite': paper = rp.push_paper(paper, overwrite=True) + ui.info(('Paper `{}` was overwritten.'.format(citekey))) break # else edit again # Also handle malformed bibtex and metadata diff --git a/pubs/commands/import_cmd.py b/pubs/commands/import_cmd.py index 9ccb935..fa6236e 100644 --- a/pubs/commands/import_cmd.py +++ b/pubs/commands/import_cmd.py @@ -13,6 +13,10 @@ from ..uis import get_ui 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): parser = subparsers.add_parser('import', 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") parser.add_argument('-O', '--overwrite', action='store_true', default=False, 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 -def many_from_path(bibpath): +def many_from_path(ui, bibpath, ignore=False): """Extract list of papers found in bibliographic files in path. The behavior is to: @@ -49,16 +55,31 @@ 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: + 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 = {} for b in biblist: for k, b in b.items(): + if k in papers: + ui.warning('Duplicated citekey {}. Keeping the last one.'.format(k)) try: papers[k] = Paper(k, b) papers[k].added = datetime.datetime.now() 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 @@ -75,20 +96,17 @@ def command(conf, args): rp = repo.Repository(conf) # 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() for k in keys: p = papers[k] - if isinstance(p, Exception): - ui.error('Could not load entry for citekey {}.'.format(k)) + rp.push_paper(p, overwrite=args.overwrite) + 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: - rp.push_paper(p, overwrite=args.overwrite) - 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: - rp.push_doc(p.citekey, docfile, copy=copy) - #FIXME should move the file if configured to do so. + rp.push_doc(p.citekey, docfile, copy=copy) + # FIXME should move the file if configured to do so. rp.close() diff --git a/pubs/databroker.py b/pubs/databroker.py index d529c3e..97c1398 100644 --- a/pubs/databroker.py +++ b/pubs/databroker.py @@ -45,7 +45,11 @@ class DataBroker(object): def pull_bibentry(self, 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): metadata_raw = self.endecoder.encode_metadata(metadata) 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/repo.py b/pubs/repo.py index fd942b0..51cd666 100644 --- a/pubs/repo.py +++ b/pubs/repo.py @@ -141,6 +141,13 @@ class Repository(object): pass def rename_paper(self, paper, new_citekey=None, old_citekey=None): + """Move a paper from a citekey to another one. + + Even if the new and old citekey are the same, the paper instance is + pushed to disk. + + :return: True if a rename happened, False if not. + """ if old_citekey is None: old_citekey = paper.citekey if new_citekey is None: @@ -149,6 +156,7 @@ class Repository(object): # check if new_citekey is not the same as paper.citekey if old_citekey == new_citekey: self.push_paper(paper, overwrite=True, event=False) + return False else: # check if new_citekey does not exists if new_citekey in self: @@ -171,6 +179,7 @@ class Repository(object): self.remove_paper(old_citekey, event=False) # send event events.RenameEvent(paper, old_citekey).send() + return True def push_doc(self, citekey, docfile, copy=None): p = self.pull_paper(citekey) diff --git a/pubs/uis.py b/pubs/uis.py index c2a8bfc..9a39b32 100644 --- a/pubs/uis.py +++ b/pubs/uis.py @@ -11,11 +11,11 @@ import subprocess from . import color from . import config from .p3 import _get_raw_stdout, _get_raw_stderr, input, ustr -from .content import check_file, read_text_file, write_file, system_path +from .content import check_file, read_text_file, write_file DEBUG = False # unhandled exceptions traces are printed -DEBUG_ALL_TRACES = False # handled exceptions traces are printed +DEBUG_ALL_TRACES = False # handled exceptions traces are printed # package-shared ui that can be accessed using : # from uis import get_ui # ui = get_ui() @@ -38,40 +38,14 @@ def _get_encoding(conf): def _get_local_editor(): """Get the editor from environment variables. - Use nano as a default. + Use vi as a default. """ - return os.environ.get('EDITOR', 'nano') - - -def _editor_input(editor, initial='', suffix='.tmp'): - """Use an editor to get input""" - str_initial = initial.encode('utf-8') # TODO: make it a configuration item - with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file: - tfile_name = temp_file.name - temp_file.write(str_initial) - cmd = shlex.split(editor) # this enable editor command with option, e.g. gvim -f - cmd.append(tfile_name) - subprocess.call(cmd) - content = read_text_file(tfile_name) - os.remove(tfile_name) - return content - - -def _edit_file(editor, path_to_file, temporary=True): - if temporary: - check_file(path_to_file, fail=True) - content = read_text_file(path_to_file) - content = _editor_input(editor, content) - write_file(path_to_file, content) - else: - cmd = editor.split() # this enable editor command with option, e.g. gvim -f - cmd.append(system_path(path_to_file)) - subprocess.call(cmd) + return os.environ.get('EDITOR', 'vi') def get_ui(): if _ui is None: - return PrintUI(config.load_default_conf()) # no editor support. (#FIXME?) + return PrintUI(config.load_default_conf()) # no editor support. (#FIXME?) return _ui @@ -111,7 +85,7 @@ class PrintUI(object): kwargs['file'] = self._stderr print('{}: {}'.format(color.dye_err('error', 'error'), message), **kwargs) - if DEBUG_ALL_TRACES: # if an exception has been raised, print the trace. + if DEBUG_ALL_TRACES: # if an exception has been raised, print the trace. if sys.exc_info()[0] is not None: traceback.print_exception(*sys.exc_info) @@ -130,6 +104,7 @@ class PrintUI(object): self.exit() return True # never happens + class InputUI(PrintUI): """UI class. Stores configuration parameters and system information. """ @@ -144,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 @@ -184,7 +159,6 @@ class InputUI(PrintUI): pass self.message('Incorrect option.', option_str) - def input_choice(self, options, option_chars, default=None, question=''): """Ask the user to chose between a set of options. The user is asked to input a char corresponding to the option he chooses. @@ -223,7 +197,41 @@ class InputUI(PrintUI): return [True, False][answer] def editor_input(self, initial="", suffix='.tmp'): - return _editor_input(self.editor, initial=initial, suffix=suffix) + """Use an editor to get input""" + str_initial = initial.encode('utf-8') # TODO: make it a configuration item + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file: + tfile_name = temp_file.name + temp_file.write(str_initial) + self._call_editor(tfile_name) + content = read_text_file(tfile_name) + os.remove(tfile_name) + return content def edit_file(self, path, temporary): - _edit_file(self.editor, path, temporary=temporary) + if temporary: + check_file(path, fail=True) + content = read_text_file(path) + content = self.editor_input(content) + write_file(path, content) + else: + self._call_editor(path) + + def _call_editor(self, path): + """Call the editor, and checks that no error were raised by the OS""" + cmd = shlex.split(self.editor) # this enable editor command with option, e.g. gvim -f + cmd.append(path) + try: + subprocess.call(cmd) + except OSError as e: + if e.errno == os.errno.ENOENT: + self.error(("Error while calling editor '{}'. The editor may " + "not be present. You can change the text editor " + "that pubs uses by setting the $EDITOR environment " + "variable, or by running `pubs conf` and setting " + "the `edit_cmd` field." + ).format(self.editor)) + # handle file not found error. + self.exit() + else: + # Something else went wrong while trying to run `wget` + self.handle_exception(e) diff --git a/readme.md b/readme.md index 1129229..4b03dcd 100644 --- a/readme.md +++ b/readme.md @@ -125,3 +125,4 @@ You can access the self-documented configuration by using `pubs conf`, and all t - [Tyler Earnest](https://github.com/tmearnest) - [Dennis Wilson](https://github.com/d9w) - [Bill Flynn](https://github.com/wflynny) +- [ksunden](https://github.com/ksunden) diff --git a/tests/fake_env.py b/tests/fake_env.py index 4f8af1b..d099a79 100644 --- a/tests/fake_env.py +++ b/tests/fake_env.py @@ -74,12 +74,21 @@ class FakeInput(): def as_global(self): for md in self.module_list: md.input = self - md._editor_input = self - md._edit_file = self.input_to_file - # if mdname.endswith('files'): - # md.editor_input = self + if md.__name__ == 'pubs.uis': + md.InputUI.editor_input = self + md.InputUI.edit_file = self.input_to_file + # Do not catch UnexpectedInput + original_handler = md.InputUI.handle_exception - def input_to_file(self, _, path_to_file, temporary=True): + 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): content.write_file(path_to_file, self()) def add_input(self, inp): @@ -98,11 +107,16 @@ class TestFakeFs(fake_filesystem_unittest.TestCase): def setUp(self): self.rootpath = os.path.abspath(os.path.dirname(__file__)) + self.homepath = os.path.expanduser('~') self.setUpPyfakefs() - self.fs.CreateDirectory(os.path.expanduser('~')) - self.fs.CreateDirectory(self.rootpath) - os.chdir(self.rootpath) + self.reset_fs() def reset_fs(self): - self._stubber.tearDown() # renew the filesystem - self.setUp() + """Reset the fake filesystem""" + for dir_name in self.fs.listdir('/'): + if dir_name not in ['var', 'tmp']: + self.fs.remove_object(os.path.join('/', dir_name)) + + self.fs.create_dir(os.path.expanduser('~')) + self.fs.create_dir(self.rootpath) + os.chdir(self.rootpath) diff --git a/tests/requirements.txt b/tests/requirements.txt index 2790d94..f3726e3 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,5 +1,5 @@ # those are the additional packages required to run the tests six -pyfakefs==3.3 +pyfakefs ddt mock diff --git a/tests/str_fixtures.py b/tests/str_fixtures.py index 03360b8..3b6ded3 100644 --- a/tests/str_fixtures.py +++ b/tests/str_fixtures.py @@ -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}, title = {Z-Forcing: Training Stochastic Recurrent Networks}, 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 = """ [main] diff --git a/tests/test_apis.py b/tests/test_apis.py index 8c2017c..c2893cc 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_bibstruct.py b/tests/test_bibstruct.py index 7d56feb..05af028 100644 --- a/tests/test_bibstruct.py +++ b/tests/test_bibstruct.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import os import unittest import copy diff --git a/tests/test_config.py b/tests/test_config.py index 23856a2..cecd75b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,7 +2,7 @@ import unittest import dotdot -from pubs.config import conf +# from pubs.config import conf # class TestConfig(unittest.TestCase): # diff --git a/tests/test_databroker.py b/tests/test_databroker.py index 57e5c66..8891931 100644 --- a/tests/test_databroker.py +++ b/tests/test_databroker.py @@ -5,8 +5,7 @@ import os import dotdot import fake_env -from pubs import content, filebroker, databroker, datacache -from pubs.config import conf +from pubs import content, databroker, datacache import str_fixtures from pubs import endecoder @@ -35,7 +34,7 @@ class TestDataBroker(fake_env.TestFakeFs): self.assertEqual(db.pull_metadata('citekey1'), page99_metadata) pulled = db.pull_bibentry('citekey1')['Page99'] - for key, value in pulled.items(): + for key in pulled.keys(): self.assertEqual(pulled[key], page99_bibentry['Page99'][key]) self.assertEqual(db.pull_bibentry('citekey1'), page99_bibentry) @@ -91,4 +90,4 @@ class TestDataBroker(fake_env.TestFakeFs): if __name__ == '__main__': - unittest.main() + unittest.main(verbosity=2) diff --git a/tests/test_datacache.py b/tests/test_datacache.py index 4709aca..bfb9223 100644 --- a/tests/test_datacache.py +++ b/tests/test_datacache.py @@ -3,7 +3,6 @@ import unittest import time import dotdot -import fake_env from pubs.datacache import CacheEntrySet diff --git a/tests/test_endecoder.py b/tests/test_endecoder.py index 80a9f53..a6e3043 100644 --- a/tests/test_endecoder.py +++ b/tests/test_endecoder.py @@ -52,7 +52,7 @@ class TestEnDecode(unittest.TestCase): 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 `month={December}`""" decoder = endecoder.EnDecoder() @@ -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_pretty.py b/tests/test_pretty.py index 0d2cf23..0c09211 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -1,10 +1,8 @@ from __future__ import unicode_literals import unittest -import os import dotdot -import fake_env from pubs import endecoder, pretty, color, config diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 8091f69..43cbf2c 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -15,14 +15,12 @@ from pyfakefs.fake_filesystem import FakeFileOpen import dotdot import fake_env -from pubs import pubs_cmd, update, color, content, filebroker, uis, p3, endecoder +from pubs import pubs_cmd, color, content, uis, p3, endecoder from pubs.config import conf -import configobj import str_fixtures import fixtures -from pubs.commands import init_cmd, import_cmd # makes the tests very noisy PRINT_OUTPUT = False @@ -45,9 +43,8 @@ class FakeSystemExit(Exception): "Exited with code: {}.".format(self.code), *args) -# code for fake fs - -class TestFakeInput(unittest.TestCase): +class TestInput(unittest.TestCase): + """Test that the fake input mechanisms work correctly in the tests""" def test_input(self): input = fake_env.FakeInput(['yes', 'no']) @@ -65,13 +62,16 @@ class TestFakeInput(unittest.TestCase): color.input() def test_editor_input(self): + sample_conf = conf.load_default_conf() + ui = uis.InputUI(sample_conf) + other_input = fake_env.FakeInput(['yes', 'no'], - module_list=[uis, color]) + module_list=[uis]) other_input.as_global() - self.assertEqual(uis._editor_input(), 'yes') - self.assertEqual(uis._editor_input(), 'no') + self.assertEqual(ui.editor_input('fake_editor'), 'yes') + self.assertEqual(ui.editor_input('fake_editor'), 'no') with self.assertRaises(fake_env.FakeInput.UnexpectedInput): - color.input() + ui.editor_input() class CommandTestCase(fake_env.TestFakeFs): @@ -82,7 +82,6 @@ class CommandTestCase(fake_env.TestFakeFs): def setUp(self, nsec_stat=True): super(CommandTestCase, self).setUp() os.stat_float_times(nsec_stat) - # self.fs = fake_env.create_fake_fs([content, filebroker, conf, init_cmd, import_cmd, configobj, update], nsec_stat=nsec_stat) self.default_pubs_dir = os.path.expanduser('~/.pubs') self.default_conf_path = os.path.expanduser('~/.pubsrc') @@ -129,7 +128,7 @@ class CommandTestCase(fake_env.TestFakeFs): outs.append(color.undye(actual_out)) else: 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( actual_cmd)) return outs @@ -178,7 +177,7 @@ class URLContentTestCase(DataCommandTestCase): return p3.urlparse(url).path def url_exists(self, url): - return self.fs.Exists(self._url_to_path(url)) + return self.fs.exists(self._url_to_path(url)) def url_to_byte_content(self, url, ui=None): path = self._url_to_path(url) @@ -198,7 +197,7 @@ class TestAlone(CommandTestCase): def test_alone_misses_command(self): with self.assertRaises(FakeSystemExit) as cm: self.execute_cmds(['pubs']) - self.assertEqual(cm.exception.code, 2) + self.assertEqual(cm.exception.code, 2) def test_alone_prints_help(self): # capturing the output of `pubs --help` is difficult because argparse @@ -335,11 +334,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): @@ -355,8 +363,8 @@ class TestList(DataCommandTestCase): def test_list_several_no_date(self): self.execute_cmds(['pubs init -p testrepo']) - os.chdir('/') # weird fix for shutil.rmtree invocation. - shutil.rmtree(self.rootpath + '/testrepo') + os.chdir('/') # weird fix for shutil.rmtree invocation. + shutil.rmtree(os.path.join(self.rootpath, 'testrepo')) os.chdir(self.rootpath) self.fs.add_real_directory(os.path.join(self.rootpath, 'testrepo'), read_only=False) @@ -680,16 +688,40 @@ class TestUsecase(DataCommandTestCase): self.assertFileContentEqual(os.path.expanduser('~/.pubsrc'), str_fixtures.sample_conf) - def test_editor_abort(self): + def test_editor_aborts(self): with self.assertRaises(FakeSystemExit): cmds = ['pubs init', - ('pubs add', ['abc', 'n']), - ('pubs add', ['abc', 'y', 'abc', 'n']), '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) + 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]), @@ -791,6 +823,25 @@ class TestUsecase(DataCommandTestCase): outs = self.execute_cmds(cmds) 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): cmds = ['pubs init', 'pubs add data/pagerank.bib', @@ -920,4 +971,4 @@ class TestCache(DataCommandTestCase): if __name__ == '__main__': - unittest.main() + unittest.main(verbosity=2)