diff --git a/papers/commands/remove_cmd.py b/papers/commands/remove_cmd.py index a40e40a..e1dfea4 100644 --- a/papers/commands/remove_cmd.py +++ b/papers/commands/remove_cmd.py @@ -1,7 +1,10 @@ from .. import repo from .. import color +from .. import configs from .helpers import add_references_argument, parse_references +from ..events import RemoveEvent + def parser(subparsers, config): parser = subparsers.add_parser('remove', help='removes a paper') @@ -18,4 +21,7 @@ def command(config, ui, references): sure = ui.input_yn(question=are_you_sure, default='n') if sure: for c in citekeys: + rmevent = RemoveEvent(config, ui, c) + rmevent.send() + rp.remove(c) diff --git a/papers/events.py b/papers/events.py new file mode 100644 index 0000000..efd94d6 --- /dev/null +++ b/papers/events.py @@ -0,0 +1,34 @@ +_listener = {} + + +class Event(object): + """Base event that can be sent to listeners. + """ + + def send(self): + """ This function sends the instance of the class, i.e. the event + to be sent, to all function that listen to it. + """ + if self.__class__.__name__ in _listener: + for f, args in _listener[self.__class__.__name__]: + f(self, *args) + + @classmethod + def listen(cls, *args): + def wrap(f): + if cls.__name__ not in _listener: + _listener[cls.__name__] = [] + _listener[cls.__name__].append((f, args)) + + # next step allow us to call the function itself without Event raised + def wrapped_f(*args): + f(*args) + return wrapped_f + return wrap + + +class RemoveEvent(Event): + def __init__(self, config, ui, citekey): + self.config = config + self.ui = ui + self.citekey = citekey diff --git a/papers/papers b/papers/papers index 1399f67..89c78de 100755 --- a/papers/papers +++ b/papers/papers @@ -8,6 +8,7 @@ import collections from papers.ui import UI from papers import configs from papers import commands +from papers import plugin cmds = collections.OrderedDict([ ('init', commands.init_cmd), @@ -29,18 +30,16 @@ config = configs.read_config() ui = UI(config) # Extend with plugin commands -plugs = configs.get_plugins(config) -for plugname in plugs: - module_name = 'papers.plugins.' + plugname + '.' + plugname + '_cmd' - plug = __import__(module_name, globals(), locals(), - ['parser', 'command'], -1) - cmds.update(collections.OrderedDict([(plugname, plug)])) +plugin.load_plugins(config, ui, configs.get_plugins(config)) +for p in plugin.get_plugins().values(): + cmds.update(collections.OrderedDict([(p.name, p)])) +# parser = argparse.ArgumentParser(description="research papers repository") subparsers = parser.add_subparsers(title="valid commands", dest="command") for cmd_mod in cmds.values(): - subparser = cmd_mod.parser(subparsers, config) + subparser = cmd_mod.parser(subparsers, config) # why do we return the subparser ? args = parser.parse_args() args.config = config diff --git a/papers/plugin.py b/papers/plugin.py new file mode 100644 index 0000000..7794e3f --- /dev/null +++ b/papers/plugin.py @@ -0,0 +1,79 @@ +import importlib + +PLUGIN_NAMESPACE = 'plugs' + +_classes = [] +_instances = {} + + +class PapersPlugin(object): + """The base class for all plugins. Plugins provide + functionality by defining a subclass of PapersPlugin and overriding + the abstract methods defined here. + """ + def __init__(self, config, ui): + """Perform one-time plugin setup. + """ + self.name = self.__module__.split('.')[-1] + self.config = config + self.ui = ui + + #config and ui and given again to stay consistent with the core papers cmd. + #two options: + #- create specific cases in script papers/papers + #- do not store self.config and self.ui and use them if needed when command is called + #this may end up with a lot of function with config/ui in argument + #or just keep it that way... + def parser(self, subparsers, config): + """ Should retrun the parser with plugins specific command. + This is a basic example + """ + parser = subparsers.add_parser(self.name, help="echo string in argument") + parser.add_argument('strings', nargs='*', help='the strings') + return parser + + def command(self, config, ui, strings): + """This function will be called with argument defined in the parser above + This is a basic example + """ + for s in strings: + print s + + @classmethod + def get_instance(cls): + if cls in _instances: + return _instances[cls] + else: + raise RuntimeError("{} instance not created".format(cls.__name__)) + + +def load_plugins(config, ui, names): + """Imports the modules for a sequence of plugin names. Each name + must be the name of a Python module under the "PLUGIN_NAMESPACE" namespace + package in sys.path; the module indicated should contain the + PapersPlugin subclasses desired. + """ + for name in names: + modname = '%s.%s.%s.%s' % ('papers', PLUGIN_NAMESPACE, name, name) + try: + try: + namespace = importlib.import_module(modname) + except ImportError as exc: + # Again, this is hacky: + if exc.args[0].endswith(' ' + name): + ui.warning('plugin {} not found'.format(name)) + else: + raise + else: + for obj in namespace.__dict__.values(): + if isinstance(obj, type) and issubclass(obj, PapersPlugin) \ + and obj != PapersPlugin: + _classes.append(obj) + _instances[obj] = obj(config, ui) + + except: + ui.warning('error loading plugin {}'.format(name)) + + +def get_plugins(): + return _instances diff --git a/papers/plugins/__init__.py b/papers/plugs/__init__.py similarity index 100% rename from papers/plugins/__init__.py rename to papers/plugs/__init__.py diff --git a/papers/plugins/texnote/__init__.py b/papers/plugs/texnote/__init__.py similarity index 100% rename from papers/plugins/texnote/__init__.py rename to papers/plugs/texnote/__init__.py diff --git a/papers/plugins/texnote/note_sample.tex b/papers/plugs/texnote/note_sample.tex similarity index 100% rename from papers/plugins/texnote/note_sample.tex rename to papers/plugs/texnote/note_sample.tex diff --git a/papers/plugins/texnote/texnote_cmd.py b/papers/plugs/texnote/texnote.py similarity index 60% rename from papers/plugins/texnote/texnote_cmd.py rename to papers/plugs/texnote/texnote.py index 83b1ce3..da26383 100644 --- a/papers/plugins/texnote/texnote_cmd.py +++ b/papers/plugs/texnote/texnote.py @@ -1,58 +1,72 @@ -#import ConfigParser - -#from ... import configs -#cfg = configs.read_config() - -#TEXNOTE_SECTION = 'texnote' -#DEFAULT_EDIT_CMD = cfg.get(configs.MAIN_SECTION, 'edit-cmd') - -#TODO file should not be created before the end of the process to ensure everything went ok -#TODO add subparser to have more feature -#TODO add clean command to wipe out any compilation file -#TODO add function to merge several texnote in one based on a research result - import os import shutil import subprocess from ... import repo -from ...paper import NoDocumentFile from ... import configs from ... import files +from ...plugin import PapersPlugin from ...commands.helpers import add_references_argument, parse_reference +from ...events import RemoveEvent + + TEXNOTE_SECTION = 'texnote' TEXNOTE_SAMPLE_FILE = os.path.join(os.path.dirname(__file__), 'note_sample.tex') TEXNOTE_DIR = 'texnote' -def parser(subparsers, config): - parser = subparsers.add_parser('texnote', help="edit advance note in latex") - parser.add_argument('-v', '--view', action='store_true', help='open the paper in a pdf viewer', default=None) - add_references_argument(parser, single=True) - return parser - - -def command(config, ui, ref, view): - ui.print_('texnote test') - if view is not None: - subprocess.Popen(['papers', 'open', ref]) - - # check if citekey exist - open_texnote(config, ui, ref) +class TexnotePlugin(PapersPlugin): + + def parser(self, subparsers, config): + parser = subparsers.add_parser(self.name, help="edit advance note in latex") + sub = parser.add_subparsers(title="valid texnote commands", dest="texcmd") + p = sub.add_parser("remove", help="remove a reference") + add_references_argument(p, single=True) + p = sub.add_parser("edit", help="edit the reference texnote") + add_references_argument(p, single=True) + #add_references_argument(parser, single=True) + parser.add_argument('-v', '--view', action='store_true', help='open the paper in a pdf viewer', default=None) + return parser + + def command(self, config, ui, texcmd, reference, view): + if view is not None: + subprocess.Popen(['papers', 'open', reference]) + if texcmd == 'edit': + open_texnote(config, ui, reference) + + def toto(self): + print "toto" + + +@RemoveEvent.listen() +def remove(rmevent): + texplug = TexnotePlugin.get_instance() + texplug.toto() + rp = repo.Repository.from_directory(rmevent.config) + paper = rp.get_paper(parse_reference(rmevent.ui, rp, rmevent.citekey)) + if 'texnote' in paper.metadata: + try: + os.remove(paper.metadata['texnote']) + except OSError: + pass # For some reason, the texnote file didn't exist + paper.metadata.pop('texnote') + metapath = rp.path_to_paper_file(paper.citekey, 'meta') + files.save_meta(paper.metadata, metapath) def open_texnote(config, ui, ref): rp = repo.Repository.from_directory(config) paper = rp.get_paper(parse_reference(ui, rp, ref)) - if not paper.metadata.has_key('texnote'): + #ugly to recode like for the doc field + if not 'texnote' in paper.metadata: texnote_dir = os.path.join(rp.papersdir, TEXNOTE_DIR) - # if folder does not exist create it + # if folder does not exist create it, this should be relative if not os.path.exists(texnote_dir): os.mkdir(texnote_dir) texnote_path = os.path.join(texnote_dir, paper.citekey + '.tex') - paper.metadata['texnote'] = files.clean_path(texnote_path) + paper.metadata['texnote'] = files.clean_path(texnote_path) # save path in metadata metapath = rp.path_to_paper_file(paper.citekey, 'meta') files.save_meta(paper.metadata, metapath) @@ -73,7 +87,6 @@ def open_texnote(config, ui, ref): subprocess.Popen([config.get(configs.MAIN_SECTION, 'edit-cmd'), texnote_path]) - ##### ugly replace by proper ##### def format_author(author): first = author.first() @@ -88,13 +101,14 @@ def format_author(author): formatted += ' ' + last[0] return formatted + def concatenate_authors(authors): concatenated = '' for a in range(len(authors)): if len(authors) > 1 and a > 0: if a == len(authors) - 1: concatenated += 'and ' - else : + else: concatenated += ', ' concatenated += authors[a] return concatenated @@ -107,27 +121,27 @@ def autofill_texnote(texnote_path, bibentry): text = f.read() f.close() # modify with bib info - print bibentry + #print bibentry fields = bibentry.fields persons = bibentry.persons - if fields.has_key('title'): + if 'title' in fields: title_str = fields['title'] text = text.replace("TITLE", title_str) - if fields.has_key('year'): + if 'year' in fields: year_str = fields['year'] text = text.replace("YEAR", year_str) - if fields.has_key('abstract'): + if 'abstract' in fields: abstract_str = fields['abstract'] text = text.replace("ABSTRACT", abstract_str) - if persons.has_key('author'): + if 'author' in persons: authors = [] for author in persons['author']: authors.append(format_author(author)) - author_str = concatenate_authors(authors) + author_str = concatenate_authors(authors) text = text.replace("AUTHOR", author_str) # write file diff --git a/setup.py b/setup.py index c9d0a64..e0c3119 100644 --- a/setup.py +++ b/setup.py @@ -10,9 +10,9 @@ setup(name='papers', description='research papers manager', requires=['pybtex'], packages=find_packages(), + package_data={'': ['*.tex']}, scripts=['papers/papers'] ) -# TODO include package data from plugins -# Jonathan could not make it works (08/06/2013) +# TODO include proper package data from plugins (08/06/2013) # should we use MANIFEST.in or package_data = ..., or both diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 0000000..0b509a3 --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,65 @@ +from unittest import TestCase + +from papers.events import Event + + +_output = None + + +class TestEvent(Event): + def __init__(self, string): + self.string = string + + def print_one(self): + _output.append('one') + + +class AddEvent(Event): + def __init__(self): + pass + + def add(self, a, b): + return a + b + + +@TestEvent.listen(12, 15) +def display(TestEventInstance, nb1, nb2): + _output.append("%s %s %s" + % (TestEventInstance.string, nb1, nb2)) + + +@TestEvent.listen() +def hello_word(TestEventInstance): + _output.append('Helloword') + + +@TestEvent.listen() +def print_it(TestEventInstance): + TestEventInstance.print_one() + + +@AddEvent.listen() +def do_it(AddEventInstance): + _output.append(AddEventInstance.add(17, 25)) + + +class TestEvents(TestCase): + + def setUp(self): + global _output + _output = [] + + def test_listen_TestEvent(self): + # using the callback system + myevent = TestEvent('abcdefghijklmnopqrstuvwxyz') + myevent.send() # this one call three function + correct = ['abcdefghijklmnopqrstuvwxyz 12 15', + 'Helloword', + 'one'] + self.assertEquals(_output, correct) + + def test_listen_AddEvent(self): + addevent = AddEvent() + addevent.send() + correct = [42] + self.assertEquals(_output, correct)