diff --git a/pubs/commands/__init__.py b/pubs/commands/__init__.py index a9e0abe..150dae5 100644 --- a/pubs/commands/__init__.py +++ b/pubs/commands/__init__.py @@ -7,8 +7,7 @@ from . import rename_cmd from . import remove_cmd from . import list_cmd # doc -from . import attach_cmd -from . import open_cmd +from . import doc_cmd from . import tag_cmd from . import note_cmd # bulk diff --git a/pubs/commands/attach_cmd.py b/pubs/commands/attach_cmd.py deleted file mode 100644 index 99bd10b..0000000 --- a/pubs/commands/attach_cmd.py +++ /dev/null @@ -1,50 +0,0 @@ -from .. import repo -from .. import color -from ..uis import get_ui -from .. import content - -def parser(subparsers): - parser = subparsers.add_parser('attach', - help='attach a document to an existing paper') - # parser.add_argument('-c', '--copy', action='store_true', default=True, - # help="copy document files into library directory (default)") - parser.add_argument('-L', '--link', action='store_false', dest='copy', default=True, - help="don't copy document files, just create a link.") - parser.add_argument('-M', '--move', action='store_true', dest='move', default=False, - help="move document instead of of copying (ignored if --link).") - parser.add_argument('citekey', - help='citekey of the paper') - parser.add_argument('document', - help='document file') - return parser - - -def command(conf, args): - """ - :param bibfile: bibtex file (in .bib, .bibml or .yaml format. - :param docfile: path (no url yet) to a pdf or ps file - """ - - ui = get_ui() - - rp = repo.Repository(conf) - paper = rp.pull_paper(args.citekey) - - try: - document = args.document - rp.push_doc(paper.citekey, document, copy=args.copy) - if args.copy: - if args.move: - content.remove_file(document) - # else: - # if ui.input_yn('{} has been copied into pubs; should the original be removed?'.format(color.dye_out(document, 'filepath'))): - # content.remove_file(document) - - ui.message('{} attached to {}'.format(color.dye_out(document, 'filepath'), color.dye_out(paper.citekey, 'citekey'))) - - except ValueError as v: - ui.error(v.message) - ui.exit(1) - except IOError as v: - ui.error(v.message) - ui.exit(1) diff --git a/pubs/commands/doc_cmd.py b/pubs/commands/doc_cmd.py new file mode 100644 index 0000000..f221d8c --- /dev/null +++ b/pubs/commands/doc_cmd.py @@ -0,0 +1,160 @@ +import os +import subprocess + +from .. import repo +from .. import color +from ..uis import get_ui +from .. import content +from ..utils import resolve_citekey, resolve_citekey_list + +# doc --+- add $file $key [[-L|--link] | [-M|--move]] [-f|--force] +# +- remove $key [$key [...]] [-f|--force] +# +- export $key [$path] +# +- open $key [-w|--with $cmd] +# supplements attach, open + +def parser(subparsers): + doc_parser = subparsers.add_parser('doc', help='manage the document relating to a publication') + doc_subparsers = doc_parser.add_subparsers(title='document actions', help='actions to interact with the documents', + dest='action') + + add_parser = doc_subparsers.add_parser('add', help='add a document to a publication') + add_parser.add_argument('-f', '--force', action='store_true', dest='force', default=False, + help='force overwriting an already assigned document') + add_parser.add_argument('document', nargs=1, help='document file to assign') + add_parser.add_argument('citekey', nargs=1, help='citekey of the publication') + add_exclusives = add_parser.add_mutually_exclusive_group() + add_exclusives.add_argument('-L', '--link', action='store_false', dest='link', default=False, + help='do not copy document files, just create a link') + add_exclusives.add_argument('-M', '--move', action='store_true', dest='move', default=False, + help='move document instead of of copying (ignored if --link)') + + remove_parser = doc_subparsers.add_parser('remove', help='remove assigned documents from publications') + remove_parser.add_argument('citekeys', nargs='+', help='citekeys of the publications') + remove_parser.add_argument('-f', '--force', action='store_true', dest='force', default=False, + help='force removing assigned documents') + + # favor key+ path over: key + export_parser = doc_subparsers.add_parser('export', help='export assigned documents to given path') + export_parser.add_argument('citekeys', nargs='+', help='citekeys of the documents to export') + export_parser.add_argument('path', nargs=1, help='directory to export the files to') + + open_parser = doc_subparsers.add_parser('open', help='open an assigned document') + open_parser.add_argument('citekey', nargs=1, help='citekey of the document to open') + open_parser.add_argument('-w', '--with', dest='cmd', help='command to open the file with') + + return doc_parser + +def command(conf, args): + + ui = get_ui() + rp = repo.Repository(conf) + + + # print(args) + # ui.exit() + + if args.action == 'add': + citekey = resolve_citekey(rp, args.citekey[0], ui=ui, exit_on_fail=True) + paper = rp.pull_paper(citekey) + + if paper.docpath is not None and not args.force: + msg = ("The publication {} has already the document {} assigned." + os.linesep + + "Overwrite? ").format(color.dye_out(paper.citekey, 'citekey'), color.dye_out(paper.docpath, 'filepath')) + if not ui.input_yn(question=msg, default='n'): + ui.exit(0) + else: + try: + rp.remove_doc(paper.citekey) + except (ValueError, IOError) as v: + ui.error(v.message) + ui.exit(1) + + try: + document = args.document[0] + if args.link: + rp.push_doc(paper.citekey, document, copy=False) + else: + rp.push_doc(paper.citekey, document, copy=True) + if not args.link and args.move: + content.remove_file(document) + + ui.message('{} added to {}'.format(color.dye_out(document, 'filepath'), + color.dye_out(paper.citekey, 'citekey'))) + except (ValueError, IOError) as v: + ui.error(v.message) + ui.exit(1) + + elif args.action == 'remove': + + for key in resolve_citekey_list(rp, args.citekeys, ui=ui, exit_on_fail=True): + paper = rp.pull_paper(key) + + # if there is no document (and the user cares) -> inform + continue + if paper.docpath is None and not args.force: + ui.message('Publication {} has no assigned document. Not removed.'.format( + color.dye_out(paper.citekey, 'citekey'))) + continue + + if not args.force: + msg = 'Do you really want to remove {} from {} ?'.format(color.dye_out(paper.docpath, 'filepath'), + color.dye_out(paper.citekey, 'citekey')) + if not ui.input_yn(question=msg, default='n'): + continue + + try: + rp.remove_doc(paper.citekey) + except (ValueError, IOError) as v: + ui.error(v.message) + ui.exit(1) + + elif args.action == 'export': + + if os.path.isdir(args.path[0]): + path = args.path[0] + if not path.endswith('/'): + path += '/' + else: + ui.error('{} is not a directory.'.format( + color.dye_err(args.path[0], 'filepath'))) + ui.exit(1) + + for key in resolve_citekey_list(rp, args.citekeys, ui=ui, exit_on_fail=True): + try: + paper = rp.pull_paper(key) + doc = paper.docpath + + if doc is None: + ui.message('Publication {} has no document assigned.'.format( + color.dye_out(paper.citekey, 'citekey'))) + else: + real_doc_path = rp.pull_docpath(key) + dest_path = path + os.path.basename(real_doc_path) + content.copy_content(real_doc_path, dest_path) + except (ValueError, IOError) as e: + ui.error(e.message) + + elif args.action == 'open': + with_command = args.cmd + citekey = resolve_citekey(rp, args.citekey[0], ui=ui, exit_on_fail=True) + paper = rp.pull_paper(citekey) + + if paper.docpath is None: + ui.error('No document associated with the entry {}.'.format( + color.dye_err(citekey, 'citekey'))) + ui.exit() + + if with_command is None: + with_command = conf['main']['open_cmd'] + if with_command is None: # default in conf have not been changed + pass # TODO platform specific + + try: + docpath = content.system_path(rp.databroker.real_docpath(paper.docpath)) + cmd = with_command.split() + cmd.append(docpath) + subprocess.Popen(cmd) + ui.message('{} opened.'.format(color.dye_out(docpath, 'filepath'))) + except OSError: + ui.error("Command does not exist: %s." % with_command) + ui.exit(127) diff --git a/pubs/commands/open_cmd.py b/pubs/commands/open_cmd.py deleted file mode 100644 index bee1378..0000000 --- a/pubs/commands/open_cmd.py +++ /dev/null @@ -1,47 +0,0 @@ -import subprocess - -from .. import repo - -from ..uis import get_ui -from .. import color -from ..content import system_path -from ..utils import resolve_citekey - -def parser(subparsers): - parser = subparsers.add_parser('open', - help='open the paper in a pdf viewer') - parser.add_argument('-w', '--with', dest='with_command', default=None, - help='command to use to open the document file') - parser.add_argument('citekey', - help='citekey of the paper') - return parser - - -def command(conf, args): - - ui = get_ui() - with_command = args.with_command - - rp = repo.Repository(conf) - citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True) - paper = rp.pull_paper(citekey) - - if with_command is None: - with_command = conf['main']['open_cmd'] - if with_command is None: # default in conf have not been changed - pass # TODO platform specific - - if paper.docpath is None: - ui.error('No document associated with the entry {}.'.format( - color.dye_err(citekey, 'citekey'))) - ui.exit() - - try: - docpath = system_path(rp.databroker.real_docpath(paper.docpath)) - cmd = with_command.split() - cmd.append(docpath) - subprocess.Popen(cmd) - ui.message('{} opened.'.format(color.dye_out(docpath, 'filepath'))) - except OSError: - ui.error("Command does not exist: %s." % with_command) - ui.exit(127) diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index b791234..3c450b1 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -18,8 +18,7 @@ CORE_CMDS = collections.OrderedDict([ ('remove', commands.remove_cmd), ('list', commands.list_cmd), - ('attach', commands.attach_cmd), - ('open', commands.open_cmd), + ('doc', commands.doc_cmd), ('tag', commands.tag_cmd), ('note', commands.note_cmd), diff --git a/pubs/repo.py b/pubs/repo.py index d08caaa..4bf4a3d 100644 --- a/pubs/repo.py +++ b/pubs/repo.py @@ -84,22 +84,40 @@ class Repository(object): def remove_paper(self, citekey, remove_doc=True, event=True): """ Remove a paper. Is silent if nothing needs to be done.""" - if event: events.RemoveEvent(citekey).send() if remove_doc: - try: - metadata = self.databroker.pull_metadata(citekey) - docpath = metadata.get('docfile') - self.databroker.remove_doc(docpath, silent=True) - self.databroker.remove_note(citekey, silent=True) - except IOError: - pass # FXME: if IOError is about being unable to - # remove the file, we need to issue an error.I - + self.remove_doc(citekey, detach_only=True) + try: + self.databroker.remove_note(citekey, silent=True) + except IOError: + pass # FIXME: if IOError is about being unable to + # remove the file, we need to issue an error.I self.citekeys.remove(citekey) self.databroker.remove(citekey) + def remove_doc(self, citekey, detach_only=False): + """ Remove a doc. Is silent if nothing needs to be done.""" + try: + metadata = self.databroker.pull_metadata(citekey) + docpath = metadata.get('docfile') + self.databroker.remove_doc(docpath, silent=True) + if not detach_only: + p = self.pull_paper(citekey) + p.docpath = None + self.push_paper(p, overwrite=True, event=False) + except IOError: + pass # FIXME: if IOError is about being unable to + # remove the file, we need to issue an error.I + + def pull_docpath(self, citekey): + try: + p = self.pull_paper(citekey) + return self.databroker.real_docpath(p.docpath) + except IOError: + pass # FIXME: if IOError is about being unable to + # remove the file, we need to issue an error.I + def rename_paper(self, paper, new_citekey=None, old_citekey=None): if old_citekey is None: old_citekey = paper.citekey diff --git a/pubs/uis.py b/pubs/uis.py index a08126f..2db2cbd 100644 --- a/pubs/uis.py +++ b/pubs/uis.py @@ -87,8 +87,8 @@ class InputUI(PrintUI): 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 iser is asked - to input a char corresponding to the option he choses. + """Ask the user to chose between a set of options. The user is asked + to input a char corresponding to the option he chooses. :param options: list of strings list of options @@ -126,8 +126,8 @@ class InputUI(PrintUI): def input_choice(self, options, option_chars, default=None, question=''): - """Ask the user to chose between a set of options. The iser is asked - to input a char corresponding to the option he choses. + """Ask the user to chose between a set of options. The user is asked + to input a char corresponding to the option he chooses. :param options: list of strings list of options diff --git a/pubs/utils.py b/pubs/utils.py index ed06087..3e08b6f 100644 --- a/pubs/utils.py +++ b/pubs/utils.py @@ -9,18 +9,18 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True): citekeys = repo.citekeys_from_prefix(citekey) if len(citekeys) == 0: if ui is not None: - ui.error("no citekey named or beginning with '{}'".format(color.dye_out(citekey, 'citekey'))) + ui.error("No citekey named or beginning with '{}'".format(color.dye_out(citekey, 'citekey'))) if exit_on_fail: ui.exit() elif len(citekeys) == 1: if citekeys[0] != citekey: if ui is not None: - ui.warning("provided citekey '{}' has been autocompleted into '{}'".format(color.dye_out(citekey, 'citekey'), color.dye_out(citekeys[0], 'citekey'))) + ui.warning("Provided citekey '{}' has been autocompleted into '{}'".format(color.dye_out(citekey, 'citekey'), color.dye_out(citekeys[0], 'citekey'))) citekey = citekeys[0] elif citekey not in citekeys: if ui is not None: citekeys = sorted(citekeys) - ui.error("be more specific; provided citekey '{}' matches multiples citekeys:".format( + ui.error("Be more specific; Provided citekey '{}' matches multiples citekeys:".format( citekey)) for c in citekeys: p = repo.pull_paper(c) @@ -28,3 +28,18 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True): if exit_on_fail: ui.exit() return citekey + + +def resolve_citekey_list(repo, citekeys, ui=None, exit_on_fail=True): + shutdown = False + keys = [] + for key in citekeys: + try: + keys.append(resolve_citekey(repo, key, ui, exit_on_fail)) + except SystemExit: + shutdown = exit_on_fail + + if shutdown and ui is not None: + ui.exit() + else: + return keys diff --git a/readme.md b/readme.md index 9409f46..851fa42 100644 --- a/readme.md +++ b/readme.md @@ -46,7 +46,7 @@ If you use latex, you can automatize references, by creating a bash script with: bibtex manuscript latex manuscript.tex -This ensure that your reference file is always up-to-date; you can cite a paper in your manuscript a soon as you add it in pubs. This means that if you have, for instance, a doi on a webpage, you only need to do: +This ensures that your reference file is always up-to-date; you can cite a paper in your manuscript a soon as you add it in pubs. This means that if you have, for instance, a doi on a webpage, you only need to do: pubs add -D 10.1007/s00422-012-0514-6 diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 4f55c04..297a686 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -352,7 +352,7 @@ class TestUsecase(DataCommandTestCase): 'pubs add data/martius.bib', 'pubs add data/10.1371%2Fjournal.pone.0038236.bib', 'pubs list', - 'pubs attach Page99 data/pagerank.pdf' + 'pubs doc add data/pagerank.pdf Page99' ] self.execute_cmds(cmds) @@ -363,7 +363,7 @@ class TestUsecase(DataCommandTestCase): 'pubs add data/martius.bib', 'pubs add data/10.1371%2Fjournal.pone.0038236.bib', 'pubs list', - 'pubs attach Page99 data/pagerank.pdf', + 'pubs doc add data/pagerank.pdf Page99', ('pubs remove Page99', ['y']), 'pubs remove -f turing1950computing', ] @@ -461,19 +461,6 @@ class TestUsecase(DataCommandTestCase): outs = self.execute_cmds(cmds) self.assertEqual(1 + 1, len(outs[-1].split('\n'))) - def test_open(self): - cmds = ['pubs init', - 'pubs add data/pagerank.bib', - 'pubs open Page99' - ] - - with self.assertRaises(SystemExit): - self.execute_cmds(cmds) - - with self.assertRaises(SystemExit): - cmds[-1] == 'pubs open Page8' - self.execute_cmds(cmds) - def test_update(self): cmds = ['pubs init', 'pubs add data/pagerank.bib', @@ -491,10 +478,21 @@ class TestUsecase(DataCommandTestCase): outs = self.execute_cmds(cmds) self.assertEqual(1, len(outs[2].splitlines())) - def test_attach(self): + def test_doc_open(self): cmds = ['pubs init', 'pubs add data/pagerank.bib', - 'pubs attach Page99 data/pagerank.pdf' + 'pubs doc open Page99' + ] + with self.assertRaises(SystemExit): + self.execute_cmds(cmds) + with self.assertRaises(SystemExit): + cmds[-1] == 'pubs doc open Page8' + self.execute_cmds(cmds) + + def test_doc_add(self): + cmds = ['pubs init', + 'pubs add data/pagerank.bib', + 'pubs doc add data/pagerank.pdf Page99' ] self.execute_cmds(cmds) self.assertTrue(self.fs['os'].path.exists( @@ -504,15 +502,36 @@ class TestUsecase(DataCommandTestCase): # Also test that do not remove original self.assertTrue(self.fs['os'].path.exists('/data/pagerank.pdf')) - def test_attach_with_move(self): + def test_doc_add_with_move(self): cmds = ['pubs init -p paper_second/', 'pubs add data/pagerank.bib', - 'pubs attach --move Page99 data/pagerank.pdf' + 'pubs doc add --move data/pagerank.pdf Page99' ] self.execute_cmds(cmds) self.assertTrue(self.fs['os'].path.isfile(self.default_conf_path)) self.assertFalse(self.fs['os'].path.exists('/data/pagerank.pdf')) + def test_doc_remove(self): + cmds = ['pubs init', + 'pubs add data/pagerank.bib', + 'pubs doc add data/pagerank.pdf Page99', + ('pubs doc remove Page99', ['y']), + ] + self.execute_cmds(cmds) + docdir = self.fs['os'].path.expanduser('~/.pubs/doc/') + self.assertNotIn('turing-mind-1950.pdf', self.fs['os'].listdir(docdir)) + + def test_doc_export(self): + cmds = ['pubs init', + 'pubs add data/pagerank.bib', + 'pubs rename Page99 page100', + 'pubs doc add data/pagerank.pdf page100', + 'pubs doc export page100 /' + ] + self.execute_cmds(cmds) + self.assertIn('page100.pdf', self.fs['os'].listdir('/')) + + def test_alternate_config(self): alt_conf = self.fs['os'].path.expanduser('~/.alt_conf') cmds = ['pubs -c ' + alt_conf + ' init',