diff --git a/TODO b/TODO index 2f22223..03f876f 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,6 @@ TODO list ========= - manage cross-references -+ labels - find (authors) duplicates + remove command - stats command diff --git a/papers/color.py b/papers/color.py index 5c2a078..794d028 100644 --- a/papers/color.py +++ b/papers/color.py @@ -24,7 +24,7 @@ filepath = cyan def dye(s, color=end, bold=False): assert color[0] == '\033' if bold: - s = '\033[1' + s[3:] + color = '\033[1' + color[3:] return color + s + end _dye = dye diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index bde2380..26691fa 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -8,5 +8,6 @@ import open_cmd import edit_cmd import remove_cmd import websearch_cmd -import tags_cmd +import tag_cmd import attach_cmd +import update_cmd diff --git a/papers/commands/add_cmd.py b/papers/commands/add_cmd.py index 41d4126..8a97537 100644 --- a/papers/commands/add_cmd.py +++ b/papers/commands/add_cmd.py @@ -10,7 +10,7 @@ def parser(subparsers, config): parser.add_argument('-b', '--bibfile', help='bibtex, bibtexml or bibyaml file', default=None) parser.add_argument('-d', '--docfile', help='pdf or ps file', default=None) - parser.add_argument('-l', '--label', help='label associated to the paper', + parser.add_argument('-t', '--tags', help='tags associated to the paper, separated by commas', default=None) parser.add_argument('-c', '--copy', action='store_true', default=None, help="copy document files into library directory (default)") @@ -19,7 +19,7 @@ def parser(subparsers, config): return parser -def command(config, ui, bibfile, docfile, label, copy): +def command(config, ui, bibfile, docfile, tags, copy): """ :param bibfile: bibtex file (in .bib, .bibml or .yaml format. :param docfile: path (no url yet) to a pdf or ps file @@ -37,15 +37,15 @@ def command(config, ui, bibfile, docfile, label, copy): cont = False except Exception: cont = ui.input_yn( - question='Invalid bibfile. Edit again or abort?', + question='Invalid bibfile. Edit again ?', default='y') if not cont: ui.exit() p = Paper(bibentry=bib, citekey=key) else: p = Paper.load(bibfile) - if label is not None: - p.metadata['labels'] = label.split() + if tags is not None: + p.tags = set(tags.split(',')) # Check if another doc file is specified in bibtex docfile2 = extract_doc_path_from_bibdata(p, ui) if docfile is None: diff --git a/papers/commands/helpers.py b/papers/commands/helpers.py index c857fd3..33988b4 100644 --- a/papers/commands/helpers.py +++ b/papers/commands/helpers.py @@ -1,5 +1,6 @@ from .. import files from .. import color +from .. import pretty from ..repo import InvalidReference from ..paper import NoDocumentFile @@ -51,3 +52,16 @@ def parse_reference(ui, rp, ref): def parse_references(ui, rp, refs): citekeys = [parse_reference(ui, rp, ref) for ref in refs] return citekeys + +def paper_oneliner(p, n = 0, citekey_only = False): + if citekey_only: + return p.citekey + else: + bibdesc = pretty.bib_oneliner(p.bibentry) + return (u'{num:d}: [{citekey}] {descr} {tags}'.format( + num=int(n), + citekey=color.dye(p.citekey, color.purple), + descr=bibdesc, + tags=color.dye(' '.join(p.tags), + color.purple, bold=True), + )).encode('utf-8') diff --git a/papers/commands/init_cmd.py b/papers/commands/init_cmd.py index a4ccba8..948a66b 100644 --- a/papers/commands/init_cmd.py +++ b/papers/commands/init_cmd.py @@ -24,13 +24,14 @@ def command(config, ui, path, doc_dir): else: papersdir = os.path.join(os.getcwd(), path) configs.add_and_write_option('papers', 'papers-directory', papersdir) - if not os.path.exists(papersdir): - ui.print_('Initializing papers in {}.'.format( - color.dye(papersdir, color.filepath))) - repo = Repository() - repo.init(papersdir) # Creates directories - repo.save() # Saves empty repository description - else: - ui.error('papers already present in {}.'.format( - color.dye(papersdir, color.filepath))) - ui.exit() + if os.path.exists(papersdir): + if len(os.listdir(papersdir)) > 0: + ui.error('directory {} is not empty.'.format( + color.dye(papersdir, color.filepath))) + ui.exit() + + ui.print_('Initializing papers in {}.'.format( + color.dye(papersdir, color.filepath))) + repo = Repository() + repo.init(papersdir) # Creates directories + repo.save() # Saves empty repository description \ No newline at end of file diff --git a/papers/commands/list_cmd.py b/papers/commands/list_cmd.py index 37946d2..bfa8edd 100644 --- a/papers/commands/list_cmd.py +++ b/papers/commands/list_cmd.py @@ -1,6 +1,7 @@ from .. import pretty from .. import repo from .. import color +from . import helpers def parser(subparsers, config): @@ -9,7 +10,7 @@ def parser(subparsers, config): default=False, dest='citekeys', help='Only returns citekeys of matching papers.') parser.add_argument('query', nargs='*', - help='Paper query (e.g. "year: 2000" or "labels: math")') + help='Paper query (e.g. "year: 2000" or "tags: math")') return parser @@ -17,20 +18,7 @@ def command(config, ui, citekeys, query): rp = repo.Repository.from_directory(config) papers = [(n, p) for n, p in enumerate(rp.all_papers()) if test_paper(query, p)] - if citekeys: - paper_strings = [p.citekey for n, p in papers] - else: - paper_strings = [] - for n, p in papers: - bibdesc = pretty.bib_oneliner(p.bibentry) - paper_strings.append((u'{num:d}: [{citekey}] {descr} {labels}'.format( - num=int(n), - citekey=color.dye(p.citekey, color.purple), - descr=bibdesc, - labels=color.dye(' '.join(p.metadata.get('labels', [])), - color.purple, bold=True), - )).encode('utf-8')) - ui.print_('\n'.join(paper_strings)) + ui.print_('\n'.join(helpers.paper_oneliner(p, n = n, citekey_only = citekeys) for n, p in papers)) # TODO author is not implemented, should we do it by last name only or more @@ -45,8 +33,8 @@ def test_paper(query_string, p): field = tmp[0] value = tmp[1] - if field in ['labels', 'l', 'tags', 't']: - if value not in p.metadata['labels']: + 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: diff --git a/papers/commands/tag_cmd.py b/papers/commands/tag_cmd.py new file mode 100644 index 0000000..398d8a2 --- /dev/null +++ b/papers/commands/tag_cmd.py @@ -0,0 +1,59 @@ +""" +This command is all about tags. +The different use cases are : +1. > papers tag + Returns the list of all tags +2. > papers tag citekey + Return the list of tags of the given citekey +3. > papers tag citekey math + Add 'math' to the list of tags of the given citekey +4. > papers tag citekey :math + Remove 'math' for the list of tags of the given citekey +5. > papers tag citekey math,romance,:war + Add 'math' and 'romance' tags to the given citekey, and remove the 'war' tag +6. > papers tag math + If 'math' is not a citekey, then display all papers with the tag 'math' +""" + +from ..repo import Repository, InvalidReference +from . import helpers + +def parser(subparsers, config): + parser = subparsers.add_parser('tag', help="add, remove and show tags") + parser.add_argument('referenceOrTag', nargs='?', default = None, + help='reference to the paper (citekey or number), or ' + 'tag.') + parser.add_argument('tags', nargs='?', default = None, + help='If the previous argument was a reference, then ' + 'then a list of tags separated by commas.') + # TODO find a way to display clear help for multiple command semantics, + # indistinguisable for argparse. (fabien, 201306) + return parser + +def command(config, ui, referenceOrTag, tags): + """Add, remove and show tags""" + rp = Repository.from_directory(config) + + if referenceOrTag is None: + for tag in rp.get_tags(): + ui.print_(tag) + else: + try: + citekey = rp.citekey_from_ref(referenceOrTag) + p = rp.get_paper(citekey) + if tags is None: + ui.print_(' '.join(p.tags)) + else: + tags = tags.split(',') + for tag in tags: + if tag[0] == ':': + p.remove_tag(tag[1:]) + else: + p.add_tag(tag) + rp.save_paper(p) + except InvalidReference: + tag = referenceOrTag + papers_list = [(p, n) for n, p in enumerate(rp.all_papers()) + if tag in p.tags] + ui.print_('\n'.join(helpers.paper_oneliner(p, n) + for p, n in papers_list)) \ No newline at end of file diff --git a/papers/commands/tags_cmd.py b/papers/commands/tags_cmd.py deleted file mode 100644 index ce43ee7..0000000 --- a/papers/commands/tags_cmd.py +++ /dev/null @@ -1,13 +0,0 @@ -from ..repo import Repository - - -def parser(subparsers, config): - parser = subparsers.add_parser('tags', help="list existing tags") - return parser - - -def command(config, ui): - """List existing tags""" - rp = Repository.from_directory(config) - for tag in rp.get_labels(): - ui.print_(tag) diff --git a/papers/commands/update_cmd.py b/papers/commands/update_cmd.py new file mode 100644 index 0000000..6c521b9 --- /dev/null +++ b/papers/commands/update_cmd.py @@ -0,0 +1,19 @@ +from .. import repo +from .. import color + +def parser(subparsers, config): + parser = subparsers.add_parser('update', help='update the repository to the lastest format') + return parser + + +def command(config, ui): + rp = repo.Repository.from_directory(config) + msg = ("You should backup the paper directory {} before continuing." + "Continue ?").format(color.dye(rp.papersdir, color.filepath)) + sure = ui.input_yn(question=msg, default='n') + if sure: + for p in rp.all_papers(): + tags = set(p.metadata['tags']) + tags = tags.union(p.metadata['labels']) + p.metadata.pop('labels', []) + rp.save_paper(p) diff --git a/papers/commands/websearch_cmd.py b/papers/commands/websearch_cmd.py index f12b70f..be05e4e 100644 --- a/papers/commands/websearch_cmd.py +++ b/papers/commands/websearch_cmd.py @@ -5,12 +5,13 @@ import urllib def parser(subparsers, config): parser = subparsers.add_parser('websearch', help="launch a search on Google Scholar") - parser.add_argument("search_string", + parser.add_argument("search_string", nargs = '*', help="the search query (anything googly is possible)") return parser def command(config, ui, search_string): + print search_string url = ("https://scholar.google.fr/scholar?q=%s&lr=" - % (urllib.quote_plus(search_string))) + % (urllib.quote_plus(' '.join(search_string)))) webbrowser.open(url) diff --git a/papers/files.py b/papers/files.py index 0af1cda..8be8a5d 100644 --- a/papers/files.py +++ b/papers/files.py @@ -1,6 +1,7 @@ import os import subprocess import tempfile +from cStringIO import StringIO import yaml @@ -21,10 +22,10 @@ try: import pybtex.database.output.bibyaml except ImportError: - print(ui.dye('error', ui.error) + - ": you need to install Pybtex; try running 'pip install" + print(color.dye('error', color.error) + + ": you need to install Pybtex; try running 'pip install " "pybtex' or 'easy_install pybtex'") - + exit(-1) _papersdir = None @@ -122,7 +123,7 @@ def load_externalbibfile(fullbibpath): filename, ext = os.path.splitext(os.path.split(fullbibpath)[1]) if ext[1:] in FORMATS_INPUT.keys(): with open(fullbibpath) as f: - return parse_bibdata(f, ext[1:]) + return _parse_bibdata_formated_stream(f, ext[1:]) else: print('{}: {} not recognized format for bibliography'.format( color.dye('error', color.error), @@ -130,14 +131,38 @@ def load_externalbibfile(fullbibpath): exit(-1) -def parse_bibdata(content, format_): - """Parse bib data from string. - +def _parse_bibdata_formated_stream(stream, fmt): + """Parse a stream for bibdata, using the supplied format.""" + try: + parser = FORMATS_INPUT[fmt].Parser() + data = parser.parse_stream(stream) + if data.entries.keys() > 0: + return data + except Exception: + pass + raise ValueError, 'content format is not recognized.' + +def parse_bibdata(content, format_ = None): + """Parse bib data from string or stream. + + Raise ValueError if no bibdata is present. :content: stream - :param format_: (bib|xml|yml) + :param format_: (bib|xml|yml) if format is None, tries to recognize the + format automatically. """ - parser = FORMATS_INPUT[format_].Parser() - return parser.parse_stream(content) + fmts = [format_] + if format_ is None: + fmts = FORMATS_INPUT.keys() + # we need to reuse the content + content = content if type(content) == str else str(content.read()) + + for fmt in fmts: + try: + return _parse_bibdata_formated_stream(StringIO(content), fmt) + except Exception: + pass + + raise ValueError, 'content format is not recognized.' def editor_input(config, initial="", suffix=None): diff --git a/papers/paper.py b/papers/paper.py index 2f003e6..d0405a5 100644 --- a/papers/paper.py +++ b/papers/paper.py @@ -20,7 +20,7 @@ CITEKEY_EXCLUDE_RE = re.compile('[%s]' BASE_META = { 'external-document': None, - 'labels': [], + 'tags': [], 'notes': [], } @@ -44,7 +44,7 @@ def get_bibentry_from_file(bibfile): def get_bibentry_from_string(content): """Extract first entry (supposed to be the only one) from given file. """ - bib_data = files.parse_bibdata(StringIO(content), 'yml') + bib_data = files.parse_bibdata(StringIO(content)) first_key = bib_data.entries.keys()[0] first_entry = bib_data.entries[first_key] return first_key, first_entry @@ -69,6 +69,7 @@ def get_safe_metadata(meta): base_meta = Paper.create_meta() if meta is not None: base_meta.update(meta) + base_meta['tags'] = set(base_meta['tags']) return base_meta @@ -250,7 +251,27 @@ class Paper(object): return papers -class PaperInRepo(Paper): + # tags + + @property + def tags(self): + return self.metadata.setdefault('tags', set()) + + @tags.setter + def tags(self, value): + if not hasattr(value, '__iter__'): + raise ValueError, 'tags must be iterables' + self.metadata['tags'] = set(value) + + def add_tag(self, tag): + self.tags.add(tag) + + def remove_tag(self, tag): + """Remove a tag from a paper if present.""" + self.tags.discard(tag) + + +class PaperInRepo(Paper): # TODO document why this class exists (fabien, 2013/06) def __init__(self, repo, *args, **kwargs): Paper.__init__(self, *args, **kwargs) diff --git a/papers/papers b/papers/papers index 865eec0..45f0d0c 100755 --- a/papers/papers +++ b/papers/papers @@ -11,18 +11,19 @@ from papers import commands from papers import plugins cmds = collections.OrderedDict([ - ('init', commands.init_cmd), - ('add', commands.add_cmd), + ('init', commands.init_cmd), + ('add', commands.add_cmd), ('add_library', commands.add_library_cmd), - ('import', commands.import_cmd), - ('export', commands.export_cmd), - ('list', commands.list_cmd), - ('edit', commands.edit_cmd), - ('remove', commands.remove_cmd), - ('open', commands.open_cmd), - ('websearch', commands.websearch_cmd), - ('tags', commands.tags_cmd), - ('attach', commands.attach_cmd), + ('import', commands.import_cmd), + ('export', commands.export_cmd), + ('list', commands.list_cmd), + ('edit', commands.edit_cmd), + ('remove', commands.remove_cmd), + ('open', commands.open_cmd), + ('websearch', commands.websearch_cmd), + ('tag', commands.tag_cmd), + ('attach', commands.attach_cmd), + ('update', commands.update_cmd), ]) config = configs.read_config() diff --git a/papers/repo.py b/papers/repo.py index 4db7a67..6486890 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -198,11 +198,11 @@ class Repository(object): new_doc_file = os.path.join(doc_path, citekey + ext) shutil.copy(doc_file, new_doc_file) - def get_labels(self): - labels = set() + def get_tags(self): + tags = set() for p in self.all_papers(): - labels = labels.union(p.metadata.get('labels', [])) - return labels + tags = tags.union(p.tags) + return tags @classmethod def from_directory(cls, config, papersdir=None): diff --git a/tests/test_paper.py b/tests/test_paper.py index be99a90..43b37c6 100644 --- a/tests/test_paper.py +++ b/tests/test_paper.py @@ -24,7 +24,7 @@ entries: META = """ external-document: null notes: [] -labels: [] +tags: [] """