diff --git a/pubs/__init__.py b/pubs/__init__.py index 2b8877c..ef7eb44 100644 --- a/pubs/__init__.py +++ b/pubs/__init__.py @@ -1 +1 @@ -__version__ = '0.5.0' +__version__ = '0.6.0' diff --git a/pubs/color.py b/pubs/color.py index 2319e6a..ef22452 100644 --- a/pubs/color.py +++ b/pubs/color.py @@ -27,6 +27,7 @@ def generate_colors(stream, color=True, bold=True, italic=True): colors[u'bold'] = u'' colors[u'italic'] = u'' colors[u'end'] = u'' + colors[u''] = u'' if (color or bold or italic) and _color_supported(stream): bold_flag, italic_flag = '', '' @@ -36,6 +37,8 @@ def generate_colors(stream, color=True, bold=True, italic=True): if italic: colors['italic'] = u'\033[3m' italic_flag = '3;' + if bold and italic: + colors['bolditalic'] = u'\033[1;3m' for i, name in enumerate(COLOR_LIST): if color: @@ -69,10 +72,17 @@ def dye_err(s, color='end'): def _nodye(s, *args, **kwargs): return s -def setup(color=False, bold=False, italic=False): +def setup(conf): global COLORS_OUT, COLORS_ERR - COLORS_OUT = generate_colors(sys.stdout, color=color, bold=bold, italic=italic) - COLORS_ERR = generate_colors(sys.stderr, color=color, bold=bold, italic=italic) + COLORS_OUT = generate_colors(sys.stdout, color=conf['formating']['color'], + bold=conf['formating']['bold'], + italic=conf['formating']['italics']) + COLORS_ERR = generate_colors(sys.stderr, color=conf['formating']['color'], + bold=conf['formating']['bold'], + italic=conf['formating']['italics']) + for key, value in conf['theme'].items(): + COLORS_OUT[key] = COLORS_OUT.get(value, '') + COLORS_ERR[key] = COLORS_ERR.get(value, '') # undye undye_re = re.compile('\x1b\[[;\d]*[A-Za-z]') @@ -80,10 +90,3 @@ undye_re = re.compile('\x1b\[[;\d]*[A-Za-z]') def undye(s): """Purge string s of color""" return undye_re.sub('', s) - -# colors -ok = 'green' -error = 'red' -citekey = 'purple' -filepath = 'bold' -tag = 'cyan' diff --git a/pubs/commands/__init__.py b/pubs/commands/__init__.py index 60f514f..a9e0abe 100644 --- a/pubs/commands/__init__.py +++ b/pubs/commands/__init__.py @@ -1,5 +1,7 @@ # core from . import init_cmd +from . import conf_cmd + from . import add_cmd from . import rename_cmd from . import remove_cmd @@ -16,4 +18,3 @@ from . import import_cmd from . import websearch_cmd from . import edit_cmd -# from . import update_cmd diff --git a/pubs/commands/add_cmd.py b/pubs/commands/add_cmd.py index 9c2ea28..6267a47 100644 --- a/pubs/commands/add_cmd.py +++ b/pubs/commands/add_cmd.py @@ -1,5 +1,4 @@ from ..uis import get_ui -from ..configs import config from .. import bibstruct from .. import content from .. import repo @@ -28,12 +27,12 @@ def parser(subparsers): return parser -def bibentry_from_editor(ui, rp): +def bibentry_from_editor(conf, ui, rp): again = True bibstr = templates.add_bib while again: try: - bibstr = content.editor_input(config().edit_cmd, + bibstr = content.editor_input(conf['main']['edit_cmd'], bibstr, suffix='.bib') if bibstr == templates.add_bib: @@ -57,7 +56,7 @@ def bibentry_from_editor(ui, rp): return bibentry -def command(args): +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 @@ -69,12 +68,12 @@ def command(args): tags = args.tags citekey = args.citekey - rp = repo.Repository(config()) + rp = repo.Repository(conf) # get bibtex entry if bibfile is None: if args.doi is None and args.isbn is None: - bibentry = bibentry_from_editor(ui, rp) + bibentry = bibentry_from_editor(conf, ui, rp) else: if args.doi is not None: bibentry_raw = apis.doi2bibtex(args.doi) @@ -124,18 +123,28 @@ def command(args): '{}, using {} instead.').format(bib_docfile, docfile)) # create the paper + copy = args.copy + if copy is None: + copy = conf['main']['doc_add'] in ('copy', 'move') + move = args.move + if move is None: + move = conf['main']['doc_add'] == 'move' try: rp.push_paper(p) if docfile is not None: - rp.push_doc(p.citekey, docfile, copy=args.copy or args.move) - if args.copy: - if args.move: + rp.push_doc(p.citekey, docfile, copy=copy or args.move) + if copy: + if move: content.remove_file(docfile) - # elif ui.input_yn('{} has been copied into pubs; should the original be removed?'.format(color.dye_out(docfile, 'bold'))): - # content.remove_file(docfile) ui.message('added to pubs:\n{}'.format(pretty.paper_oneliner(p))) + if copy: + if move: + ui.message('{} was moved to the pubs repository.'.format(docfile)) + else: + ui.message('{} was copied to the pubs repository.'.format(docfile)) + except ValueError as v: ui.error(v.message) ui.exit(1) diff --git a/pubs/commands/attach_cmd.py b/pubs/commands/attach_cmd.py index d0ea5bf..99bd10b 100644 --- a/pubs/commands/attach_cmd.py +++ b/pubs/commands/attach_cmd.py @@ -1,6 +1,5 @@ from .. import repo from .. import color -from ..configs import config from ..uis import get_ui from .. import content @@ -20,7 +19,7 @@ def parser(subparsers): return parser -def command(args): +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 @@ -28,7 +27,7 @@ def command(args): ui = get_ui() - rp = repo.Repository(config()) + rp = repo.Repository(conf) paper = rp.pull_paper(args.citekey) try: @@ -38,10 +37,10 @@ def command(args): 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, 'bold'))): + # 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, 'bold'), color.dye_out(paper.citekey, color.citekey))) + ui.message('{} attached to {}'.format(color.dye_out(document, 'filepath'), color.dye_out(paper.citekey, 'citekey'))) except ValueError as v: ui.error(v.message) diff --git a/pubs/commands/conf_cmd.py b/pubs/commands/conf_cmd.py new file mode 100644 index 0000000..d09d61c --- /dev/null +++ b/pubs/commands/conf_cmd.py @@ -0,0 +1,36 @@ +from .. import uis +from .. import config +from .. import content + + +def parser(subparsers): + parser = subparsers.add_parser('conf', + help='open the configuration in an editor') + return parser + + +def command(conf, args): + uis.init_ui(conf) + ui = uis.get_ui() + + while True: + # get modif from user + content.edit_file(conf['main']['edit_cmd'], config.get_confpath()) + + new_conf = config.load_conf(check=False) + try: + config.check_conf(new_conf) + ui.message('The configuration file was updated.') + break + except AssertionError: # TODO better error message + ui.error('Error reading the modified configuration file.') + options = ['edit_again', 'abort'] + choice = options[ui.input_choice( + options, ['e', 'a'], + question=('Edit again or abort? If you abort, the changes will be reverted.') + )] + + if choice == 'abort': + config.save_conf(conf) + ui.message('The changes have been reverted.') + break diff --git a/pubs/commands/edit_cmd.py b/pubs/commands/edit_cmd.py index 497665f..522b837 100644 --- a/pubs/commands/edit_cmd.py +++ b/pubs/commands/edit_cmd.py @@ -1,6 +1,6 @@ from ..paper import Paper from .. import repo -from ..configs import config + from ..uis import get_ui from ..endecoder import EnDecoder from ..utils import resolve_citekey @@ -16,12 +16,12 @@ def parser(subparsers): return parser -def command(args): +def command(conf, args): ui = get_ui() meta = args.meta - rp = repo.Repository(config()) + rp = repo.Repository(conf) citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True) paper = rp.pull_paper(citekey) diff --git a/pubs/commands/export_cmd.py b/pubs/commands/export_cmd.py index 0e87296..a55a791 100644 --- a/pubs/commands/export_cmd.py +++ b/pubs/commands/export_cmd.py @@ -1,7 +1,6 @@ from __future__ import print_function from .. import repo -from ..configs import config from ..uis import get_ui from .. import endecoder @@ -14,14 +13,14 @@ def parser(subparsers): return parser -def command(args): +def command(conf, args): """ """ # :param bib_format (only 'bibtex' now) ui = get_ui() - rp = repo.Repository(config()) + rp = repo.Repository(conf) try: papers = [rp.pull_paper(c) for c in args.citekeys] diff --git a/pubs/commands/import_cmd.py b/pubs/commands/import_cmd.py index c34de89..ceda2bd 100644 --- a/pubs/commands/import_cmd.py +++ b/pubs/commands/import_cmd.py @@ -6,7 +6,7 @@ from .. import endecoder from .. import bibstruct from .. import color from ..paper import Paper -from ..configs import config + from ..uis import get_ui from ..content import system_path, read_file @@ -58,7 +58,7 @@ def many_from_path(bibpath): return papers -def command(args): +def command(conf, args): """ :param bibpath: path (no url yet) to a bibliography file """ @@ -66,10 +66,10 @@ def command(args): ui = get_ui() bibpath = args.bibpath copy = args.copy - if copy is None: - copy = config().import_copy - rp = repo.Repository(config()) + copy = conf['main']['doc_add'] in ('copy', 'move') + + rp = repo.Repository(conf) # Extract papers from bib papers = many_from_path(bibpath) keys = args.keys or papers.keys() @@ -80,12 +80,13 @@ def command(args): ui.error('could not load entry for citekey {}.'.format(k)) else: rp.push_paper(p) - ui.message('{} imported'.format(color.dye_out(p.citekey, color.citekey))) + ui.message('{} 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=args.copy) + rp.push_doc(p.citekey, docfile, copy=copy) + #FIXME should move the file if configured to do so. except KeyError: ui.error('no entry found for citekey {}.'.format(k)) except IOError as e: diff --git a/pubs/commands/init_cmd.py b/pubs/commands/init_cmd.py index 416f535..048e713 100644 --- a/pubs/commands/init_cmd.py +++ b/pubs/commands/init_cmd.py @@ -2,25 +2,25 @@ import os -from ..configs import config + from ..uis import get_ui from .. import color from ..repo import Repository from ..content import system_path, check_directory - +from .. import config def parser(subparsers): parser = subparsers.add_parser('init', help="initialize the pubs directory") parser.add_argument('-p', '--pubsdir', default=None, - help='path to pubs directory (if none, ~/.ubs is used)') + help='directory where to put the pubs repository (if none, ~/.pubs is used)') parser.add_argument('-d', '--docsdir', default='docsdir://', help=('path to document directory (if not specified, documents will' 'be stored in /path/to/pubsdir/doc/)')) return parser -def command(args): +def command(conf, args): """Create a .pubs directory""" ui = get_ui() @@ -34,13 +34,15 @@ def command(args): if check_directory(pubsdir, fail=False) and len(os.listdir(pubsdir)) > 0: ui.error('directory {} is not empty.'.format( - color.dye_err(pubsdir, color.filepath))) + color.dye_err(pubsdir, 'filepath'))) ui.exit() - ui.message('Initializing pubs in {}'.format(color.dye_out(pubsdir, color.filepath))) + ui.message('Initializing pubs in {}'.format(color.dye_out(pubsdir, 'filepath'))) - config().pubsdir = pubsdir - config().docsdir = docsdir - config().save() + conf['main']['pubsdir'] = pubsdir + conf['main']['docsdir'] = docsdir + conf['main']['open_cmd'] = config.default_open_cmd() + conf['main']['edit_cmd'] = config.default_edit_cmd() + config.save_conf(conf) - Repository(config(), create=True) + Repository(conf, create=True) diff --git a/pubs/commands/list_cmd.py b/pubs/commands/list_cmd.py index 5b648aa..9e7063b 100644 --- a/pubs/commands/list_cmd.py +++ b/pubs/commands/list_cmd.py @@ -3,7 +3,6 @@ from datetime import datetime from .. import repo from .. import pretty from .. import bibstruct -from ..configs import config from ..uis import get_ui @@ -28,7 +27,7 @@ def parser(subparsers): help='list only pubs without attached documents.') parser.add_argument('query', nargs='*', - help='Paper query (e.g. "year: 2000" or "tags: math")') + help='Paper query ("author:Einstein", "title:learning", "year:2000" or "tags:math")') return parser @@ -36,9 +35,9 @@ def date_added(p): return p.added or datetime(1, 1, 1) -def command(args): +def command(conf, args): ui = get_ui() - rp = repo.Repository(config()) + rp = repo.Repository(conf) papers = filter(lambda p: filter_paper(p, args.query, case_sensitive=args.case_sensitive), rp.all_papers()) diff --git a/pubs/commands/note_cmd.py b/pubs/commands/note_cmd.py index ad2a3eb..77be6d2 100644 --- a/pubs/commands/note_cmd.py +++ b/pubs/commands/note_cmd.py @@ -1,8 +1,8 @@ from .. import repo from .. import content -from ..configs import config from ..uis import get_ui + def parser(subparsers): parser = subparsers.add_parser('note', help='edit the note attached to a paper') @@ -11,17 +11,16 @@ def parser(subparsers): return parser -def command(args): +def command(conf, args): """ """ ui = get_ui() - - rp = repo.Repository(config()) + rp = repo.Repository(conf) if not rp.databroker.exists(args.citekey): ui.error("citekey {} not found".format(args.citekey)) ui.exit(1) notepath = rp.databroker.real_notepath(args.citekey) - content.edit_file(config().edit_cmd, notepath, temporary=False) + content.edit_file(conf['main']['edit_cmd'], notepath, temporary=False) diff --git a/pubs/commands/open_cmd.py b/pubs/commands/open_cmd.py index 9fb0afc..bee1378 100644 --- a/pubs/commands/open_cmd.py +++ b/pubs/commands/open_cmd.py @@ -1,7 +1,7 @@ import subprocess from .. import repo -from ..configs import config + from ..uis import get_ui from .. import color from ..content import system_path @@ -17,21 +17,23 @@ def parser(subparsers): return parser -def command(args): +def command(conf, args): ui = get_ui() with_command = args.with_command - rp = repo.Repository(config()) + 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 = config().open_cmd + 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, color.citekey))) + color.dye_err(citekey, 'citekey'))) ui.exit() try: @@ -39,7 +41,7 @@ def command(args): cmd = with_command.split() cmd.append(docpath) subprocess.Popen(cmd) - ui.message('{} opened.'.format(color.dye_out(docpath, color.filepath))) + 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/remove_cmd.py b/pubs/commands/remove_cmd.py index a4245c2..ec39f48 100644 --- a/pubs/commands/remove_cmd.py +++ b/pubs/commands/remove_cmd.py @@ -1,6 +1,5 @@ from .. import repo from .. import color -from ..configs import config from ..uis import get_ui @@ -13,21 +12,21 @@ def parser(subparsers): return parser -def command(args): +def command(conf, args): ui = get_ui() force = args.force - rp = repo.Repository(config()) + rp = repo.Repository(conf) if force is None: are_you_sure = (("Are you sure you want to delete paper(s) [{}]" " (this will also delete associated documents)?") - .format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys]))) + .format(', '.join([color.dye_out(c, 'citekey') for c in args.citekeys]))) sure = ui.input_yn(question=are_you_sure, default='n') if force or sure: for c in args.citekeys: rp.remove_paper(c) - ui.message('The paper(s) [{}] were removed'.format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys]))) + ui.message('The paper(s) [{}] were removed'.format(', '.join([color.dye_out(c, 'citekey') for c in args.citekeys]))) # FIXME: print should check that removal proceeded well. else: - ui.message('The paper(s) [{}] were *not* removed'.format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys]))) + ui.message('The paper(s) [{}] were *not* removed'.format(', '.join([color.dye_out(c, 'citekey') for c in args.citekeys]))) diff --git a/pubs/commands/rename_cmd.py b/pubs/commands/rename_cmd.py index 0540544..bc2df2d 100644 --- a/pubs/commands/rename_cmd.py +++ b/pubs/commands/rename_cmd.py @@ -1,5 +1,4 @@ from ..uis import get_ui -from ..configs import config from .. import bibstruct from .. import content from .. import repo @@ -14,14 +13,14 @@ def parser(subparsers): return parser -def command(args): +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(config()) + rp = repo.Repository(conf) paper = rp.pull_paper(args.citekey) rp.rename_paper(paper, args.new_citekey) diff --git a/pubs/commands/tag_cmd.py b/pubs/commands/tag_cmd.py index 7e07b94..2cd8339 100644 --- a/pubs/commands/tag_cmd.py +++ b/pubs/commands/tag_cmd.py @@ -20,7 +20,6 @@ The different use cases are : import re from ..repo import Repository -from ..configs import config from ..uis import get_ui from .. import pretty from .. import color @@ -70,23 +69,22 @@ def _tag_groups(tags): minus_tags.append(tag[1:]) return set(plus_tags), set(minus_tags) - -def command(args): +def command(conf, args): """Add, remove and show tags""" ui = get_ui() citekeyOrTag = args.citekeyOrTag tags = args.tags - rp = Repository(config()) + rp = Repository(conf) if citekeyOrTag is None: - ui.message(color.dye_out(' '.join(sorted(rp.get_tags())), color.tag)) + ui.message(color.dye_out(' '.join(sorted(rp.get_tags())), 'tag')) else: if rp.databroker.exists(citekeyOrTag): p = rp.pull_paper(citekeyOrTag) if tags is None: - ui.message(color.dye_out(' '.join(sorted(p.tags)), color.tag)) + ui.message(color.dye_out(' '.join(sorted(p.tags)), 'tag')) else: add_tags, remove_tags = _tag_groups(_parse_tag_seq(tags)) for tag in add_tags: diff --git a/pubs/commands/update_cmd.py b/pubs/commands/update_cmd.py deleted file mode 100644 index bc1d899..0000000 --- a/pubs/commands/update_cmd.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys - -from .. import repo -from .. import color -from ..configs import config -from ..uis import get_ui -from ..__init__ import __version__ - -def parser(subparsers): - parser = subparsers.add_parser('update', help='update the repository to the lastest format') - return parser - - -def command(args): - - ui = get_ui() - - code_version = __version__ - repo_version = int(config().version) - - if repo_version == code_version: - ui.message('Your pubs repository is up-to-date.') - sys.exit(0) - elif repo_version > code_version: - ui.message('Your repository was generated with an newer version of pubs.\n' - 'You should not use pubs until you install the newest version.') - sys.exit(0) - else: - msg = ("You should backup the pubs directory {} before continuing." - "Continue ?").format(color.dye_out(config().papers_dir, color.filepath)) - sure = ui.input_yn(question=msg, default='n') - if not sure: - sys.exit(0) - - -# config().version = repo_version -# config().save() diff --git a/pubs/commands/websearch_cmd.py b/pubs/commands/websearch_cmd.py index e3e1c76..ee7ee20 100644 --- a/pubs/commands/websearch_cmd.py +++ b/pubs/commands/websearch_cmd.py @@ -11,7 +11,7 @@ def parser(subparsers): return parser -def command(args): +def command(conf, args): ui = get_ui() search_string = args.search_string diff --git a/pubs/config/__init__.py b/pubs/config/__init__.py new file mode 100644 index 0000000..5de91fb --- /dev/null +++ b/pubs/config/__init__.py @@ -0,0 +1,2 @@ +from .conf import get_confpath, load_default_conf, load_conf, save_conf, check_conf +from .conf import default_open_cmd, default_edit_cmd diff --git a/pubs/config/conf.py b/pubs/config/conf.py new file mode 100644 index 0000000..6c41e21 --- /dev/null +++ b/pubs/config/conf.py @@ -0,0 +1,102 @@ +import os +import platform +import shutil + + +import configobj +import validate + +from .spec import configspec + + +DFT_CONFIG_PATH = os.path.expanduser('~/.pubsrc') + + +def load_default_conf(): + """Load the default configuration""" + default_conf = configobj.ConfigObj(configspec=configspec) + validator = validate.Validator() + default_conf.validate(validator, copy=True) + return default_conf + + +def get_confpath(verify=True): + """Return the configuration filepath + If verify is True, verify that the file exists and exit with an error if not. + """ + confpath = DFT_CONFIG_PATH + if 'PUBSCONF' in os.environ: + confpath = os.path.abspath(os.path.expanduser(os.environ['PUBSCONF'])) + if verify: + if not os.path.isfile(confpath): + from .. import uis + ui = uis.get_ui() + ui.error('configuration file not found at `{}`'.format(confpath)) + ui.exit(error_code=1) + return confpath + + +def check_conf(conf): + """Type check a configuration""" + validator = validate.Validator() + results = conf.validate(validator, copy=True) + assert results == True, '{}'.format(results) # TODO: precise error dialog when parsing error + + +def load_conf(check=True, path=None): + """Load the configuration""" + if path is None: + path = get_confpath(verify=True) + with open(path, 'rb') as f: + conf = configobj.ConfigObj(f.readlines(), configspec=configspec) + if check: + check_conf(conf) + conf.filename = path + return conf + + +def save_conf(conf, path=None): + """Save the configuration.""" + if path is not None: + conf.filename = path + elif conf.filename is None: + conf.filename = get_confpath(verify=False) + with open(conf.filename, 'wb') as f: + conf.write(outfile=f) + + +def default_open_cmd(): + """Chooses the default command to open documents""" + if platform.system() == 'Darwin': + return 'open' + elif platform.system() == 'Linux': + return 'xdg-open' + elif platform.system() == 'Windows': + return 'start' + else: + return None + + +def which(cmd): + try: + return shutil.which(cmd) # available in python 3.3 + except AttributeError: + for path in ['.'] + os.environ["PATH"].split(os.pathsep): + filepath = os.path.join(path.strip('"'), cmd) + if os.path.isfile(path) and os.access(path, os.X_OK): + return filepath + return None + + +def default_edit_cmd(): + """Find an available editor""" + if 'EDITOR' in os.environ: + return os.environ['EDITOR'] + elif platform.system() == 'Darwin' or 'Linux': + for editor in ['vim', 'nano', 'emacs', 'vi']: + if which(editor) is not None: + return editor + elif platform.system() == 'Windows': + return 'Notepad.exe | Out-Null' # wait for notepad to close + else: + return None diff --git a/pubs/config/spec.py b/pubs/config/spec.py new file mode 100644 index 0000000..27cf449 --- /dev/null +++ b/pubs/config/spec.py @@ -0,0 +1,86 @@ +from .. import __version__ + +configspec = """ +[main] + +# Where the pubs repository files (bibtex, metadata, notes) are located +pubsdir = string(default='~/pubs') + +# Where the documents files are located (default: $(pubsdir)/doc/) +docsdir = string(default="docsdir://") + +# Specify if a document should be copied or moved in the docdir, or only +# linked when adding a publication. +doc_add = option('copy', 'move', 'link', default='move') + +# the command to use when opening document files +open_cmd = string(default=None) + +# which editor to use when editing bibtex files. +# if using a graphical editor, use the --wait or --block option, i.e.: +# "atom --wait" +# "kate --block" +edit_cmd = string(default=None) + + +[formating] + +# Enable bold formatting, if the terminal supports it. +bold = boolean(default=True) + +# Enable italics, if the terminal supports it. +italics = boolean(default=True) + +# Enable colors, if the terminal supports it. +color = boolean(default=True) + + +[theme] + +# Here you can define the color theme used by pubs, if enabled in the +# 'formating' section. Predefined theme are available at: +# https://github.com/pubs/pubs/blob/master/theme.md + +# Available colors are: 'black', 'red', 'green', 'yellow', 'blue', 'purple', +# 'cyan', and 'grey'. Bold colors are available by prefixing 'b' in front of +# the color name ('bblack', 'bred', etc.), italic colors by prefixing 'i', +# and bold italic by prefixing 'bi'. Finally, 'bold', 'italic' and +# 'bolditalic' can be used to apply formatting without changing the color. +# For no color, use an empty string '' + +# messages +ok = string(default='green') +warning = string(default='yellow') +error = string(default='red') + +# ui elements +filepath = string(default='bold') +citekey = string(default='purple') +tag = string(default='cyan') + +# bibliographic fields +author = string(default='bold') +title = string(default='') +publisher = string(default='italic') +year = string(default='bold') +volume = string(default='bold') +pages = string(default='') + + +[plugins] +# comma-separated list of the plugins to load +active = list(default=list()) + +[[alias]] +# new subcommands can be defined, e.g.: +# print = open --with lp +# evince = open --with evince + +# shell commands can also be defined, by prefixing them with a bang `!`, e.g: +# count = !pubs list -k | wc -l + +[internal] +# The version of this configuration file. Do not edit. +version = string(min=5, default='{}') + +""".format(__version__).split('\n') diff --git a/pubs/configs.py b/pubs/configs.py deleted file mode 100644 index 305b58c..0000000 --- a/pubs/configs.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -import sys -import collections - -from .p3 import configparser, ConfigParser, _read_config - -from .content import check_file, _open -from . import __version__ - -# constant stuff (DFT = DEFAULT) - -MAIN_SECTION = 'pubs' -DFT_CONFIG_PATH = os.path.expanduser('~/.pubsrc') -try: - DFT_EDIT_CMD = os.environ['EDITOR'] -except KeyError: - DFT_EDIT_CMD = 'vi' - -DFT_PLUGINS = '' - -DFT_CONFIG = collections.OrderedDict([ - ('pubsdir', os.path.expanduser('~/.pubs')), - ('docsdir', ''), - ('import_copy', True), - ('import_move', False), - ('color', True), - ('version', __version__), - ('version_warning', True), - ('open_cmd', 'open'), - ('edit_cmd', DFT_EDIT_CMD), - ('plugins', DFT_PLUGINS) - ]) - -BOOLEANS = {'import_copy', 'import_move', 'color', 'version_warning'} - - -# package-shared config that can be accessed using : -# from configs import config -_config = None - - -def config(section=MAIN_SECTION): - if _config is None: - raise ValueError('not config instanciated yet') - _config._section = section - return _config - - -class Config(object): - - def __init__(self, **kwargs): - object.__setattr__(self, '_section', MAIN_SECTION) # active section - object.__setattr__(self, '_cfg', ConfigParser()) - - self._cfg.add_section(self._section) - for name, value in DFT_CONFIG.items(): - self._cfg.set(self._section, name, str(value)) - - for name, value in kwargs.items(): - self.__setattr__(name, value) - - def as_global(self): - global _config - _config = self - - def load(self, path=DFT_CONFIG_PATH): - if not check_file(path, fail=False): - raise IOError(("The configuration file {} does not exist." - " Did you run 'pubs init' ?").format(path)) - b_flag = '' - if sys.version_info[0] == 2: # HACK, FIXME please - b_flag = 'b' - with _open(path, 'r{}+'.format(b_flag)) as f: - _read_config(self._cfg, f) - return self - - def save(self, path=DFT_CONFIG_PATH): - b_flag = '' - if sys.version_info[0] == 2: # HACK, FIXME please - b_flag = 'b' - with _open(path, 'w{}+'.format(b_flag)) as f: - self._cfg.write(f) - - def __setattr__(self, name, value): - if name in ('_cfg', '_section'): - object.__setattr__(self, name, value) - else: - if type(value) is bool: - BOOLEANS.add(name) - self._cfg.set(self._section, name, str(value)) - - def __getattr__(self, name): - value = self._cfg.get(self._section, name) - if name in BOOLEANS: - value = str2bool(value) - return value - - def get(self, name, default=None): - try: - return self.__getattr__(name) - except (configparser.NoOptionError, configparser.NoSectionError): - return default - - def items(self): - for name, value in self._cfg.items(self._section): - if name in BOOLEANS: - value = str2bool(value) - yield name, value - - -def str2bool(s): - return str(s).lower() in ('yes', 'true', 't', 'y', '1') diff --git a/pubs/plugins.py b/pubs/plugins.py index d7c54cd..20653bb 100644 --- a/pubs/plugins.py +++ b/pubs/plugins.py @@ -28,13 +28,13 @@ class PapersPlugin(object): raise RuntimeError("{} instance not created".format(cls.__name__)) -def load_plugins(ui, names): +def load_plugins(conf, ui): """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: + for name in conf['plugins']['active']: modname = '%s.%s.%s.%s' % ('pubs', PLUGIN_NAMESPACE, name, name) try: namespace = importlib.import_module(modname) @@ -49,7 +49,7 @@ def load_plugins(ui, names): if isinstance(obj, type) and issubclass(obj, PapersPlugin) \ and obj != PapersPlugin: _classes.append(obj) - _instances[obj] = obj() + _instances[obj] = obj(conf) def get_plugins(): diff --git a/pubs/plugs/alias/alias.py b/pubs/plugs/alias/alias.py index 0c6da43..eebf1f9 100644 --- a/pubs/plugs/alias/alias.py +++ b/pubs/plugs/alias/alias.py @@ -2,7 +2,6 @@ import subprocess import shlex from ...plugins import PapersPlugin -from ...configs import config from ...pubs_cmd import execute @@ -16,9 +15,10 @@ class Alias(object): self.parser = parser p = parser.add_parser(self.name, help='user defined command') p.add_argument('arguments', nargs='*', - help="arguments to be passed to the user defined command") + help="arguments to be passed to the user defined command") + return p - def run(self, args): + def command(self, conf, args): raise NotImplementedError @classmethod @@ -35,7 +35,7 @@ class CommandAlias(Alias): - other arguments are passed to the command """ - def run(self, args): + def command(self, conf, args): raw_args = ([args.prog] + shlex.split(self.definition + ' ' @@ -45,7 +45,7 @@ class CommandAlias(Alias): class ShellAlias(Alias): - def run(self, args): + def command(self, conf, args): """Uses a shell function so that arguments can be used in the command as shell arguments. """ @@ -59,12 +59,14 @@ class AliasPlugin(PapersPlugin): name = 'alias' - def __init__(self): + def __init__(self, conf): self.aliases = [] - for name, definition in config('alias').items(): - self.aliases.append(Alias.create_alias(name, definition)) - - def get_commands(self, parser): - for a in self.aliases: - a.parser(parser) - return [(a.name, a.run) for a in self.aliases] + if 'alias' in conf['plugins']: + for name, definition in conf['plugins']['alias'].items(): + self.aliases.append(Alias.create_alias(name, definition)) + + def update_parser(self, subparsers): + """Add subcommand to the provided subparser""" + for alias in self.aliases: + alias_parser = alias.parser(subparsers) + alias_parser.set_defaults(func=alias.command) diff --git a/pubs/pretty.py b/pubs/pretty.py index 307637a..0859b61 100644 --- a/pubs/pretty.py +++ b/pubs/pretty.py @@ -33,10 +33,11 @@ def bib_oneliner(bibdata): journal = ' ' + bibdata.get('booktitle', '') return u'{authors} \"{title}\"{journal}{year}'.format( - authors=color.dye_out(authors, 'bold'), - title=bibdata.get('title', ''), - journal=color.dye_out(journal, 'italic'), - year=' ({})'.format(bibdata['year']) if 'year' in bibdata else '', + authors=color.dye_out(authors, 'author'), + title=color.dye_out(bibdata.get('title', ''), 'title'), + journal=color.dye_out(journal, 'publisher'), + year=' ({})'.format(color.dye_out(bibdata['year'], 'year')) + if 'year' in bibdata else '' ) @@ -55,7 +56,7 @@ def paper_oneliner(p, citekey_only=False): else: bibdesc = bib_oneliner(p.bibdata) tags = '' if len(p.tags) == 0 else '| {}'.format( - ','.join(color.dye_out(t, color.tag) for t in sorted(p.tags))) + ','.join(color.dye_out(t, 'tag') for t in sorted(p.tags))) return u'[{citekey}] {descr} {tags}'.format( - citekey=color.dye_out(p.citekey, 'purple'), + citekey=color.dye_out(p.citekey, 'citekey'), descr=bibdesc, tags=tags) diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index 2cae25a..b791234 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -1,92 +1,85 @@ import sys - import argparse import collections - from . import uis -from . import configs +from . import config from . import commands +from . import update from . import plugins from .__init__ import __version__ CORE_CMDS = collections.OrderedDict([ - ('init', commands.init_cmd), - ('add', commands.add_cmd), - ('rename', commands.rename_cmd), - ('remove', commands.remove_cmd), - ('list', commands.list_cmd), - - ('attach', commands.attach_cmd), - ('open', commands.open_cmd), - ('tag', commands.tag_cmd), - ('note', commands.note_cmd), - - ('export', commands.export_cmd), - ('import', commands.import_cmd), - - ('websearch', commands.websearch_cmd), - ('edit', commands.edit_cmd), - # ('update', commands.update_cmd), - ]) - - -def _update_check(config, ui): - if config.version_warning: - code_version = __version__.split('.') - if len(config.version) == 1: # support for deprecated version scheme. - config.version = '0.{}.0'.format(config.version) - repo_version = config.version.split('.') - - if repo_version > code_version: - ui.warning( - 'your repository was generated with an newer version' - ' of pubs (v{}) than the one you are using (v{}).' - '\n'.format(repo_version, code_version) + - 'You should not use pubs until you install the ' - 'newest version. (use version_warning in you pubsrc ' - 'to bypass this error)') - sys.exit() - elif repo_version < code_version: - ui.message( - 'warning: your repository version (v{})'.format(repo_version) - + 'must be updated to version {}.\n'.format(code_version) - + "run 'pubs update'.") - sys.exit() + ('init', commands.init_cmd), + ('conf', commands.conf_cmd), + + ('add', commands.add_cmd), + ('rename', commands.rename_cmd), + ('remove', commands.remove_cmd), + ('list', commands.list_cmd), + + ('attach', commands.attach_cmd), + ('open', commands.open_cmd), + ('tag', commands.tag_cmd), + ('note', commands.note_cmd), + + ('export', commands.export_cmd), + ('import', commands.import_cmd), + + ('websearch', commands.websearch_cmd), + ('edit', commands.edit_cmd), +]) def execute(raw_args=sys.argv): - # loading config - config = configs.Config() - if len(raw_args) > 1 and raw_args[1] != 'init': + + conf_parser = argparse.ArgumentParser(prog="pubs", add_help=False) + conf_parser.add_argument("-c", "--config", help="path to config file", + type=str, metavar="FILE") + #conf_parser.add_argument("-u", "--update", help="update config if needed", + # default=False, action='store_true') + top_args, remaining_args = conf_parser.parse_known_args(raw_args[1:]) + + if top_args.config: + conf_path = top_args.config + else: + conf_path = config.get_confpath(verify=False) # will be checked on load + + # Loading config + if len(remaining_args) > 0 and remaining_args[0] != 'init': try: - config.load() + conf = config.load_conf(path=conf_path, check=False) + if update.update_check(conf, path=conf.filename): + # an update happened, reload conf. + conf = config.load_conf(path=conf_path, check=False) + config.check_conf(conf) except IOError as e: print('error: {}'.format(str(e))) sys.exit() - config.as_global() + else: + conf = config.load_default_conf() + conf.filename = conf_path - uis.init_ui(config) + uis.init_ui(conf) ui = uis.get_ui() - _update_check(config, ui) - - parser = argparse.ArgumentParser(description="research papers repository") + parser = argparse.ArgumentParser(description="research papers repository", + prog="pubs", add_help=True) + parser.add_argument('--version', action='version', version=__version__) subparsers = parser.add_subparsers(title="valid commands", dest="command") + subparsers.required = True - cmd_funcs = collections.OrderedDict() + # Populate the parser with core commands for cmd_name, cmd_mod in CORE_CMDS.items(): - cmd_mod.parser(subparsers) - cmd_funcs[cmd_name] = cmd_mod.command + cmd_parser = cmd_mod.parser(subparsers) + cmd_parser.set_defaults(func=cmd_mod.command) # Extend with plugin commands - plugins.load_plugins(ui, config.plugins.split()) + plugins.load_plugins(conf, ui) for p in plugins.get_plugins().values(): - cmd_funcs.update(p.get_commands(subparsers)) - - args = parser.parse_args(raw_args[1:]) - args.prog = parser.prog # Hack: there might be a better way... - cmd = args.command - del args.command + p.update_parser(subparsers) - cmd_funcs[cmd](args) + # Parse and run appropriate command + args = parser.parse_args(remaining_args) + args.prog = "pubs" # FIXME? + args.func(conf, args) diff --git a/pubs/repo.py b/pubs/repo.py index a2cdfcc..d08caaa 100644 --- a/pubs/repo.py +++ b/pubs/repo.py @@ -22,10 +22,10 @@ class InvalidReference(Exception): class Repository(object): - def __init__(self, config, create=False): - self.config = config + def __init__(self, conf, create=False): + self.conf = conf self._citekeys = None - self.databroker = DataCache(self.config.pubsdir, create=create) + self.databroker = DataCache(self.conf['main']['pubsdir'], create=create) @property def citekeys(self): @@ -133,7 +133,7 @@ class Repository(object): def push_doc(self, citekey, docfile, copy=None): p = self.pull_paper(citekey) if copy is None: - copy = self.config.import_copy + copy = self.conf['main']['doc_add'] in ('copy', 'move') if copy: docfile = self.databroker.add_doc(citekey, docfile) else: diff --git a/pubs/uis.py b/pubs/uis.py index df878a3..a08126f 100644 --- a/pubs/uis.py +++ b/pubs/uis.py @@ -6,6 +6,7 @@ import codecs from .content import editor_input from . import color +from . import config from .p3 import _get_raw_stdout, _get_raw_stderr, input, ustr @@ -30,7 +31,7 @@ def _get_encoding(conf): def get_ui(): if _ui is None: - return PrintUI() # no editor support. (#FIXME?) + return PrintUI(config.load_default_conf()) # no editor support. (#FIXME?) return _ui @@ -41,16 +42,12 @@ def init_ui(conf): class PrintUI(object): - def __init__(self, conf=None): + def __init__(self, conf): """ :param conf: if None, conservative default values are used. Useful to instanciate the UI before parsing the config file. """ - if conf is None: - color.setup() - else: - color.setup(color=True, bold=True, italic=True) -# color.setup(color=conf.color, bold=conf.bold, italic=conf.italic) + color.setup(conf) self.encoding = _get_encoding(conf) self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(), errors='replace') @@ -63,11 +60,11 @@ class PrintUI(object): def warning(self, message, **kwargs): kwargs['file'] = self._stderr - print('{}: {}'.format(color.dye_err('warning', 'yellow'), message), **kwargs) + print('{}: {}'.format(color.dye_err('warning', 'warning'), message), **kwargs) def error(self, message, **kwargs): kwargs['file'] = self._stderr - print('{}: {}'.format(color.dye_err('error', 'red'), message), **kwargs) + print('{}: {}'.format(color.dye_err('error', 'error'), message), **kwargs) def exit(self, error_code=1): sys.exit(error_code) @@ -79,7 +76,7 @@ class InputUI(PrintUI): def __init__(self, conf): super(InputUI, self).__init__(conf) - self.editor = conf.edit_cmd + self.editor = conf['main']['edit_cmd'] def input(self): try: @@ -101,14 +98,12 @@ class InputUI(PrintUI): :returns: int the index of the chosen option """ - char_color = 'bold' option_chars = [s[0] for s in options] displayed_chars = [c.upper() if i == default else c for i, c in enumerate(option_chars)] if len(set(option_chars)) != len(option_chars): # duplicate chars, char choices are deactivated. #FIXME: should only deactivate ambiguous chars option_chars = [] - char_color = color.end option_str = '/'.join(["{}{}".format(color.dye_out(c, 'bold'), s[1:]) for c, s in zip(displayed_chars, options)]) diff --git a/pubs/update.py b/pubs/update.py new file mode 100644 index 0000000..aab8879 --- /dev/null +++ b/pubs/update.py @@ -0,0 +1,106 @@ +import shutil + +import io +from . import config +from . import uis +from . import color +from .__init__ import __version__ + + +def update_check(conf, path=None): + """Runs an update if necessary, and return True in that case.""" + + code_version = __version__.split('.') + try: + repo_version = conf['internal']['version'].split('.') + except KeyError: + repo_version = ['0', '5', '0'] + + if repo_version > code_version: + uis.init_ui(config.load_default_conf()) + ui = uis.get_ui() + + ui.warning( + 'Your repository was generated with an newer version' + ' of pubs (v{}) than the one you are using (v{}).' + '\n'.format(repo_version, code_version) + + 'You should not use pubs until you install the ' + 'newest version.') + sys.exit() + + elif repo_version <= code_version: + return update(conf, code_version, repo_version, path=path) + + return False + +def update(conf, code_version, repo_version, path=None): + """Runs an update if necessary, and return True in that case.""" + if path is None: + path = config.get_confpath() + + if repo_version == ['0', '5', '0']: # we need to update + default_conf = config.load_default_conf() + + for key in ['pubsdir', 'docsdir', 'edit_cmd', 'open_cmd']: + if key in conf['pubs']: + default_conf['main'][key] = conf['pubs'][key] + if conf.get('import_move'): + default_conf['main']['add_doc'] = 'move' + elif conf.get('import_copy'): + default_conf['main']['add_doc'] = 'copy' + else: + default_conf['main']['add_doc'] = 'link' + + backup_path = path + '.old' + shutil.move(path, backup_path) + config.save_conf(default_conf) + + uis.init_ui(default_conf) + ui = uis.get_ui() + ui.warning( + 'Your configuration file has been updated. ' + 'Your old configuration has been moved to `{}`. '.format(color.dye_out(backup_path, 'filepath')) + + 'Some, but not all, of your settings has been transferred ' + 'to the new file.\n' + 'You can inspect and modify your configuration ' + ' using the `pubs config` command.' + ) + + return True + + # continuous update while configuration is stabilizing + if repo_version == ['0', '6', '0'] and repo_version < code_version: + default_conf = config.load_default_conf() + for section_name, section in conf.items(): + for key, value in section.items(): + try: + default_conf[section_name][key] + default_conf[section_name][key] = value + except KeyError: + pass + # we don't update plugins + for section_name, section in conf['plugins'].items(): + default_conf[section_name]['plugins'][section_name] = section + + + # comparing potential changes + with open(path, 'r') as f: + old_conf_text = f.read() + new_conf_text = io.BytesIO() + default_conf.write(outfile=new_conf_text) + + if new_conf_text.getvalue() != old_conf_text: + + backup_path = path + '.old' + shutil.move(path, backup_path) + default_conf.filename = path + config.save_conf(default_conf) + + uis.init_ui(default_conf) + ui = uis.get_ui() + ui.warning('Your configuration file has been updated.\n' + 'Your old configuration has been moved to `{}`.'.format(color.dye_out(backup_path, 'filepath'))) + + return True + + return False diff --git a/pubs/utils.py b/pubs/utils.py index 8ca1a2a..ed06087 100644 --- a/pubs/utils.py +++ b/pubs/utils.py @@ -9,13 +9,13 @@ 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, color.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, color.citekey), color.dye_out(citekeys[0], color.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: diff --git a/readme.md b/readme.md index f741495..9409f46 100644 --- a/readme.md +++ b/readme.md @@ -13,8 +13,8 @@ Pubs is built with the following principles in mind: **Notice:** pubs is still in early development and cannot be considered as stable. -Getting started ---------------- +## Getting started + Create your library (by default, goes to '~/.pubs/'). pubs init @@ -36,8 +36,8 @@ or an ISBN (dashes are ignored): pubs add -I 978-0822324669 -d article.pdf -References always up-to-date ----------------------------- +## References always up-to-date + If you use latex, you can automatize references, by creating a bash script with: #!/bin/bash @@ -52,28 +52,28 @@ This ensure that your reference file is always up-to-date; you can cite a paper and then add `\cite{Loeb_2012}` in your manuscript. After running the bash script, the citation will correctly appear in your compiled pdf. -Customization -------------- -Pubs is designed to interact well with your command line tool chain. You can add custom commands to pubs by defining aliases in your config file. Here are a few examples. +## Customization + +Pubs is designed to interact well with your command line tool chain. You can add custom commands to pubs by defining aliases in your config file. - [alias] - print = open -w lp + [[alias]] + evince = open --with evince count = !pubs list -k | wc -l -For more advanced functionalities, pubs also support plugins. Actually *alias* is itself a plugin! +The first command defines a new subcommand: `pubs open -w lp` will be executed when `pubs print` is typed. +The second starts with a bang: `!`, and is treated as a shell command. +## Requirements -Requirements ------------- - python >= 2.7 or >= 3.3 - [dateutil](http://labix.org/python-dateutil) - [pyYaml](http://pyyaml.org) (will be deprecated soon) - [bibtexparser](https://github.com/sciunto/python-bibtexparser) >= 0.6.1 -Authors -------- +## Authors - - [Fabien Benureau](http://fabien.benureau.com) - - Olivier Mangin - - Jonathan Grizou +- [Fabien Benureau](http://fabien.benureau.com) +- Olivier Mangin +- Jonathan Grizou +- Arnold Sykosch diff --git a/setup.py b/setup.py index a0a29ba..7f2525d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION = '0.5.0' +VERSION = '0.6.0' setup( name = 'pubs', @@ -13,10 +13,13 @@ setup( url = 'https://github.com/pubs/pubs', description = 'command-line scientific bibliography manager', - packages = ['pubs', 'pubs.commands', 'pubs.templates', 'pubs.plugs'], + packages = ['pubs', 'pubs.config', + 'pubs.commands', + 'pubs.templates', + 'pubs.plugs', 'pubs.plugs.alias'], scripts = ['pubs/pubs'], - install_requires = ['pyyaml', 'bibtexparser', 'python-dateutil', 'requests', + install_requires = ['pyyaml', 'bibtexparser', 'python-dateutil', 'requests', 'configobj', 'beautifulsoup4'], # to be made optional? classifiers=[ diff --git a/tests/fake_env.py b/tests/fake_env.py index c5299cf..fd627c2 100644 --- a/tests/fake_env.py +++ b/tests/fake_env.py @@ -103,6 +103,8 @@ class FakeIO(object): fakefs_stringio = self.fake_open.Call(*args, **kwargs) return UnicodeStringIOWrapper(fakefs_stringio) + BytesIO = real_io.BytesIO + StringIO = real_io.StringIO def create_fake_fs(module_list): diff --git a/tests/test_color.py b/tests/test_color.py index 0590ecf..ee86c42 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -5,7 +5,7 @@ from pubs import color def perf_color(): s = str(list(range(1000))) for _ in range(5000000): - color.dye(s, color.red) + color.dye_out(s, 'red') if __name__ == '__main__': diff --git a/tests/test_config.py b/tests/test_config.py index 6dbe176..4c49d3e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,71 +2,70 @@ import unittest import dotdot -from pubs import configs -from pubs.configs import config +from pubs.config import conf from pubs.p3 import configparser -class TestConfig(unittest.TestCase): - - def test_create_config(self): - a = configs.Config() - a.as_global() - self.assertEqual(a, config()) - - def test_config_content(self): - a = configs.Config() - a.as_global() - - self.assertEqual(config().pubsdir, configs.DFT_CONFIG['pubsdir']) - self.assertEqual(config().color, configs.str2bool(configs.DFT_CONFIG['color'])) - - def test_set(self): - a = configs.Config() - a.as_global() - config().color = 'no' - self.assertEqual(config().color, False) - self.assertEqual(config('pubs').color, False) - # booleans type for new variables are memorized, but not saved. - config().bla = True - self.assertEqual(config().bla, True) - self.assertEqual(config('pubs').bla, True) - - with self.assertRaises(configparser.NoOptionError): - config()._cfg.get(configs.MAIN_SECTION, '_section') - - def test_reload(self): - - default_color = configs.DFT_CONFIG['color'] - - a = configs.Config() - a.as_global() - a.color = False - a.bla = 'foo' - config.color = not configs.str2bool(default_color) - self.assertEqual(config().color, not configs.str2bool(default_color)) - - b = configs.Config() - b.as_global() - self.assertEqual(b, config()) - self.assertEqual(config().color, configs.str2bool(default_color)) - - def test_exception(self): - - a = configs.Config() - a.as_global() - - with self.assertRaises(configparser.NoOptionError): - config().color2 - self.assertEqual(config().get('color2', default = 'blue'), 'blue') - - with self.assertRaises(configparser.NoSectionError): - config(section = 'bla3').color - self.assertEqual(config(section = 'bla3').get('color', default = 'green'), 'green') - self.assertEqual(config(section = 'bla3').get('color', default = config().color), True) - - def test_keywords(self): - a = configs.Config(pubs_dir = '/blabla') - self.assertEqual(a.pubs_dir, '/blabla') +# class TestConfig(unittest.TestCase): +# +# def test_create_config(self): +# a = configs.Config() +# a.as_global() +# self.assertEqual(a, config()) +# +# def test_config_content(self): +# a = configs.Config() +# a.as_global() +# +# self.assertEqual(config().pubsdir, configs.DFT_CONFIG['pubsdir']) +# self.assertEqual(config().color, configs.str2bool(configs.DFT_CONFIG['color'])) +# +# def test_set(self): +# a = configs.Config() +# a.as_global() +# config().color = 'no' +# self.assertEqual(config().color, False) +# self.assertEqual(config('pubs').color, False) +# # booleans type for new variables are memorized, but not saved. +# config().bla = True +# self.assertEqual(config().bla, True) +# self.assertEqual(config('pubs').bla, True) +# +# with self.assertRaises(configparser.NoOptionError): +# config()._cfg.get(configs.MAIN_SECTION, '_section') +# +# def test_reload(self): +# +# default_color = configs.DFT_CONFIG['color'] +# +# a = configs.Config() +# a.as_global() +# a.color = False +# a.bla = 'foo' +# config.color = not configs.str2bool(default_color) +# self.assertEqual(config().color, not configs.str2bool(default_color)) +# +# b = configs.Config() +# b.as_global() +# self.assertEqual(b, config()) +# self.assertEqual(config().color, configs.str2bool(default_color)) +# +# def test_exception(self): +# +# a = configs.Config() +# a.as_global() +# +# with self.assertRaises(configparser.NoOptionError): +# config().color2 +# self.assertEqual(config().get('color2', default = 'blue'), 'blue') +# +# with self.assertRaises(configparser.NoSectionError): +# config(section = 'bla3').color +# self.assertEqual(config(section = 'bla3').get('color', default = 'green'), 'green') +# self.assertEqual(config(section = 'bla3').get('color', default = config().color), True) +# +# def test_keywords(self): +# a = configs.Config(pubs_dir = '/blabla') +# self.assertEqual(a.pubs_dir, '/blabla') if __name__ == '__main__': diff --git a/tests/test_databroker.py b/tests/test_databroker.py index e8bfa6e..09f2a60 100644 --- a/tests/test_databroker.py +++ b/tests/test_databroker.py @@ -5,7 +5,8 @@ import os import dotdot import fake_env -from pubs import content, filebroker, databroker, datacache, configs +from pubs import content, filebroker, databroker, datacache +from pubs.config import conf import str_fixtures from pubs import endecoder @@ -20,7 +21,7 @@ class TestDataBroker(unittest.TestCase): page99_bibentry = ende.decode_bibdata(str_fixtures.bibtex_raw0) for db_class in [databroker.DataBroker, datacache.DataCache]: - self.fs = fake_env.create_fake_fs([content, filebroker, configs]) + self.fs = fake_env.create_fake_fs([content, filebroker, conf]) db = db_class('tmp', create=True) diff --git a/tests/test_plug_alias.py b/tests/test_plug_alias.py new file mode 100644 index 0000000..c15b986 --- /dev/null +++ b/tests/test_plug_alias.py @@ -0,0 +1,84 @@ +import unittest + +import dotdot + +import pubs +from pubs import config +from pubs.plugs.alias.alias import (Alias, AliasPlugin, CommandAlias, + ShellAlias) + + +def to_args(arg_str): + o = lambda: None # Dirty hack + o.prog = 'pubs' + o.arguments = arg_str.split(' ') + return o + + +class FakeExecuter(object): + + called = None + executed = None + + def call(self, obj, shell=None): + self.called = obj + + def execute(self, obj): + self.executed = obj + + +class AliasTestCase(unittest.TestCase): + + def setUp(self): + self.subprocess = FakeExecuter() + pubs.plugs.alias.alias.subprocess = self.subprocess + self.cmd_execute = FakeExecuter() + pubs.plugs.alias.alias.execute = self.cmd_execute.execute + + def testAlias(self): + alias = Alias.create_alias('print', 'open -w lpppp') + alias.command(None, to_args('CiteKey')) + self.assertIsNone(self.subprocess.called) + self.assertEqual(self.cmd_execute.executed, + ['pubs', 'open', '-w', 'lpppp', 'CiteKey']) + + def testShellAlias(self): + """This actually just test that subprocess.call is called. + """ + alias = Alias.create_alias('count', '!pubs list -k | wc -l') + alias.command(None, to_args('')) + self.assertIsNone(self.cmd_execute.executed) + self.assertIsNotNone(self.subprocess.called) + + +class AliasPluginTestCase(unittest.TestCase): + + def setUp(self): + self.conf = config.load_default_conf() + self.conf['plugins']['active'] = ['alias'] + + def testAliasPluginCreated(self): + self.plugin = AliasPlugin(self.conf) + + def testAliasPluginOneCommnand(self): + self.conf['plugins']['alias'] = {'print': 'open -w lpppp'} + self.plugin = AliasPlugin(self.conf) + self.assertEqual(len(self.plugin.aliases), 1) + self.assertEqual(type(self.plugin.aliases[0]), CommandAlias) + self.assertEqual(self.plugin.aliases[0].name, 'print') + self.assertEqual(self.plugin.aliases[0].definition, 'open -w lpppp') + + def testAliasPluginOneShell(self): + self.conf['plugins']['alias'] = {'count': '!pubs list -k | wc -l'} + self.plugin = AliasPlugin(self.conf) + self.assertEqual(len(self.plugin.aliases), 1) + self.assertEqual(type(self.plugin.aliases[0]), ShellAlias) + self.assertEqual(self.plugin.aliases[0].name, 'count') + self.assertEqual(self.plugin.aliases[0].definition, + 'pubs list -k | wc -l') + + def testAliasPluginTwoCommnands(self): + self.conf['plugins']['alias'] = {'print': 'open -w lpppp', + 'count': '!pubs list -k | wc -l'} + self.plugin = AliasPlugin(self.conf) + self.assertEqual(len(self.plugin.aliases), 2) diff --git a/tests/test_pretty.py b/tests/test_pretty.py index be0648d..3985361 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -5,7 +5,7 @@ import os import dotdot import fake_env -from pubs import endecoder, pretty, color +from pubs import endecoder, pretty, color, config from str_fixtures import bibtex_raw0 @@ -13,7 +13,8 @@ from str_fixtures import bibtex_raw0 class TestPretty(unittest.TestCase): def setUp(self): - color.setup() + conf = config.load_default_conf() + color.setup(conf) def test_oneliner(self): decoder = endecoder.EnDecoder() diff --git a/tests/test_repo.py b/tests/test_repo.py index b651caf..86e175d 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -7,14 +7,15 @@ import fixtures from pubs.repo import Repository, _base27, CiteKeyCollision, InvalidReference from pubs.paper import Paper -from pubs import configs +from pubs import config class TestRepo(fake_env.TestFakeFs): def setUp(self): super(TestRepo, self).setUp() - self.repo = Repository(configs.Config(), create=True) + default_conf = config.load_default_conf() + self.repo = Repository(default_conf, create=True) self.repo.push_paper(Paper.from_bibentry(fixtures.turing_bibentry)) diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 7345918..4f55c04 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -7,8 +7,9 @@ import os import dotdot import fake_env -from pubs import pubs_cmd -from pubs import color, content, filebroker, uis, p3, endecoder, configs +from pubs import pubs_cmd, update, color, content, filebroker, uis, p3, endecoder +from pubs.config import conf +import configobj import str_fixtures import fixtures @@ -17,7 +18,7 @@ from pubs.commands import init_cmd, import_cmd # makes the tests very noisy -messagePUT=False +PRINT_OUTPUT=False CAPTURE_OUTPUT=True @@ -56,8 +57,9 @@ class CommandTestCase(unittest.TestCase): maxDiff = 1000000 def setUp(self): - self.fs = fake_env.create_fake_fs([content, filebroker, configs, init_cmd, import_cmd]) + self.fs = fake_env.create_fake_fs([content, filebroker, conf, init_cmd, import_cmd, configobj, update]) self.default_pubs_dir = self.fs['os'].path.expanduser('~/.pubs') + self.default_conf_path = self.fs['os'].path.expanduser('~/.pubsrc') def execute_cmds(self, cmds, capture_output=CAPTURE_OUTPUT): """ Execute a list of commands, and capture their output @@ -102,12 +104,12 @@ class CommandTestCase(unittest.TestCase): except fake_env.FakeInput.UnexpectedInput: self.fail('Unexpected input asked by command: {}.'.format( actual_cmd)) - if messagePUT: + if PRINT_OUTPUT: print(outs) return outs def tearDown(self): - fake_env.unset_fake_fs([content, filebroker, configs, init_cmd, import_cmd]) + fake_env.unset_fake_fs([content, filebroker, conf, init_cmd, import_cmd, configobj]) class DataCommandTestCase(CommandTestCase): @@ -323,7 +325,8 @@ class TestUsecase(DataCommandTestCase): def test_first(self): correct = ['Initializing pubs in /paper_first\n', - 'added to pubs:\n[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n', + 'added to pubs:\n[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n' + 'data/pagerank.pdf was copied to the pubs repository.\n', '[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n', '\n', '', @@ -507,8 +510,21 @@ class TestUsecase(DataCommandTestCase): 'pubs attach --move Page99 data/pagerank.pdf' ] 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_alternate_config(self): + alt_conf = self.fs['os'].path.expanduser('~/.alt_conf') + cmds = ['pubs -c ' + alt_conf + ' init', + 'pubs --config ' + alt_conf + ' import data/ Page99', + 'pubs list -c ' + alt_conf + ] + outs = self.execute_cmds(cmds) + # check if pubs works as expected + self.assertEqual(1 + 1, len(outs[-1].split('\n'))) + # check whether we actually changed the config file + self.assertFalse(self.fs['os'].path.isfile(self.default_conf_path)) + self.assertTrue(self.fs['os'].path.isfile(alt_conf)) if __name__ == '__main__': unittest.main()