From 93c54939b3d5937bf1d552ccd808691f5963ba19 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Fri, 4 Dec 2015 20:42:56 +0100 Subject: [PATCH 01/16] Rewritten config module It depends on configobj, is cleaner and simpler than the previous implementation. It adds comments in the config file, and type verification. Related: #18 --- pubs/commands/add_cmd.py | 31 +++++---- pubs/commands/attach_cmd.py | 5 +- pubs/commands/edit_cmd.py | 6 +- pubs/commands/export_cmd.py | 5 +- pubs/commands/import_cmd.py | 13 ++-- pubs/commands/init_cmd.py | 16 ++--- pubs/commands/list_cmd.py | 5 +- pubs/commands/note_cmd.py | 9 ++- pubs/commands/open_cmd.py | 10 +-- pubs/commands/remove_cmd.py | 5 +- pubs/commands/rename_cmd.py | 5 +- pubs/commands/tag_cmd.py | 6 +- pubs/commands/update_cmd.py | 18 +++--- pubs/commands/websearch_cmd.py | 2 +- pubs/config/__init__.py | 1 + pubs/config/conf.py | 49 +++++++++++++++ pubs/config/spec.py | 55 ++++++++++++++++ pubs/configs.py | 112 --------------------------------- pubs/pubs_cmd.py | 58 +++++++++-------- pubs/repo.py | 8 +-- pubs/uis.py | 12 ++-- setup.py | 7 ++- tests/test_repo.py | 5 +- tests/test_usecase.py | 11 ++-- 24 files changed, 229 insertions(+), 225 deletions(-) create mode 100644 pubs/config/__init__.py create mode 100644 pubs/config/conf.py create mode 100644 pubs/config/spec.py delete mode 100644 pubs/configs.py 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..41bcdea 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: 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..30d200e 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() @@ -85,7 +85,8 @@ def command(args): 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..0bfaa0c 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() @@ -39,8 +39,8 @@ def command(args): ui.message('Initializing pubs in {}'.format(color.dye_out(pubsdir, color.filepath))) - config().pubsdir = pubsdir - config().docsdir = docsdir - config().save() + conf['main']['pubsdir'] = pubsdir + conf['main']['docsdir'] = docsdir + 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..a382672 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 @@ -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 7f47a4a..fa4cec0 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,17 +17,19 @@ 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( diff --git a/pubs/commands/remove_cmd.py b/pubs/commands/remove_cmd.py index a4245c2..47e20d8 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,11 +12,11 @@ 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) [{}]" 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 34485b8..fb8e9a2 100644 --- a/pubs/commands/tag_cmd.py +++ b/pubs/commands/tag_cmd.py @@ -20,7 +20,7 @@ The different use cases are : import re from ..repo import Repository, InvalidReference -from ..configs import config + from ..uis import get_ui from .. import pretty from .. import color @@ -72,7 +72,7 @@ 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() @@ -80,7 +80,7 @@ def command(args): 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)) diff --git a/pubs/commands/update_cmd.py b/pubs/commands/update_cmd.py index bc1d899..0457f6a 100644 --- a/pubs/commands/update_cmd.py +++ b/pubs/commands/update_cmd.py @@ -2,7 +2,7 @@ import sys from .. import repo from .. import color -from ..configs import config + from ..uis import get_ui from ..__init__ import __version__ @@ -11,12 +11,14 @@ def parser(subparsers): return parser -def command(args): +def command(conf, args): ui = get_ui() - code_version = __version__ - repo_version = int(config().version) + code_version = __version__.split('.') + if len(conf['internal']['version']) == 1: # support for deprecated version scheme. + conf['internal']['version'] = '0.{}.0'.format(conf['internal']['version']) + repo_version = conf['internal']['version'].split('.') if repo_version == code_version: ui.message('Your pubs repository is up-to-date.') @@ -27,11 +29,11 @@ def command(args): sys.exit(0) else: msg = ("You should backup the pubs directory {} before continuing." - "Continue ?").format(color.dye_out(config().papers_dir, color.filepath)) + "Continue ?").format(color.dye_out(conf['main']['pubsdir'], color.filepath)) sure = ui.input_yn(question=msg, default='n') if not sure: sys.exit(0) - -# config().version = repo_version -# config().save() + #TODO: update!! +# conf['internal']['version'] = repo_version +# conf['internal']['version'] 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..d7ea4b7 --- /dev/null +++ b/pubs/config/__init__.py @@ -0,0 +1 @@ +from .conf import load_default_conf, load_conf, save_conf, get_pubspath diff --git a/pubs/config/conf.py b/pubs/config/conf.py new file mode 100644 index 0000000..b891fd3 --- /dev/null +++ b/pubs/config/conf.py @@ -0,0 +1,49 @@ +import os + +import configobj +import validate + +from .spec import configspec + + +DFT_CONFIG_PATH = os.path.expanduser('~/.pubsrc') + +def load_default_conf(): + """Loads the default configuration""" + default_conf = configobj.ConfigObj(configspec=configspec) + validator = validate.Validator() + default_conf.validate(validator, copy=True) + return default_conf + +def get_pubspath(verify=True): + """Returns the pubs path. + If verify is True, verify that pubs.conf exist in the directory, + 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 load_conf(check_conf=True): + """Load the user config""" + pubspath = get_pubspath(verify=True) + with open(pubspath, 'r') as f: + conf = configobj.ConfigObj(f.readlines(), configspec=configspec) + + if check_conf: + validator = validate.Validator() + results = conf.validate(validator, copy=True) + assert results == True, '{}'.format(results) # TODO: precise error dialog when parsing error + + return conf + +def save_conf(conf): + with open(get_pubspath(verify=False), 'w') as f: + conf.write(outfile=f) diff --git a/pubs/config/spec.py b/pubs/config/spec.py new file mode 100644 index 0000000..6f8be61 --- /dev/null +++ b/pubs/config/spec.py @@ -0,0 +1,55 @@ +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') + +# if True, pubs will ask confirmation before copying/moving/linking the +# document file. +doc_add_ask = boolean(default=True) + +# 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] + + + + +[plugins] +# comma-separated list of the plugins to load +active = list(default=list()) + +[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/pubs_cmd.py b/pubs/pubs_cmd.py index 2cae25a..16d7a80 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -4,7 +4,7 @@ import argparse import collections from . import uis -from . import configs +from . import config from . import commands from . import plugins from .__init__ import __version__ @@ -31,45 +31,43 @@ CORE_CMDS = collections.OrderedDict([ ]) -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() +def _update_check(conf, ui): + code_version = __version__.split('.') + if len(conf['internal']['version']) == 1: # support for deprecated version scheme. + conf['internal']['version'] = '0.{}.0'.format(conf['internal']['version']) + repo_version = conf['internal']['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.') + 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() def execute(raw_args=sys.argv): # loading config - config = configs.Config() if len(raw_args) > 1 and raw_args[1] != 'init': try: - config.load() + conf = config.load_conf(check_conf=True) except IOError as e: print('error: {}'.format(str(e))) sys.exit() - config.as_global() + else: + conf = config.load_default_conf() - uis.init_ui(config) + uis.init_ui(conf) ui = uis.get_ui() - _update_check(config, ui) + _update_check(conf, ui) parser = argparse.ArgumentParser(description="research papers repository") subparsers = parser.add_subparsers(title="valid commands", dest="command") @@ -80,7 +78,7 @@ def execute(raw_args=sys.argv): cmd_funcs[cmd_name] = cmd_mod.command # Extend with plugin commands - plugins.load_plugins(ui, config.plugins.split()) + plugins.load_plugins(ui, conf['plugins']['active']) for p in plugins.get_plugins().values(): cmd_funcs.update(p.get_commands(subparsers)) @@ -89,4 +87,4 @@ def execute(raw_args=sys.argv): cmd = args.command del args.command - cmd_funcs[cmd](args) + cmd_funcs[cmd](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 c019105..82e8744 100644 --- a/pubs/uis.py +++ b/pubs/uis.py @@ -39,16 +39,14 @@ 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(color=conf['formating']['color'], + bold=conf['formating']['bold'], + italic=conf['formating']['italics']) self.encoding = _get_encoding(conf) self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(), errors='replace') @@ -77,7 +75,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: diff --git a/setup.py b/setup.py index a0a29ba..145a58d 100644 --- a/setup.py +++ b/setup.py @@ -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'], 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/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 5464d21..38e31cf 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -8,7 +8,9 @@ import dotdot import fake_env from pubs import pubs_cmd -from pubs import color, content, filebroker, uis, p3, endecoder, configs +from pubs import color, content, filebroker, uis, p3, endecoder +from pubs.config import conf +import configobj import str_fixtures import fixtures @@ -56,7 +58,7 @@ 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]) self.default_pubs_dir = self.fs['os'].path.expanduser('~/.pubs') def execute_cmds(self, cmds, capture_output=CAPTURE_OUTPUT): @@ -107,7 +109,7 @@ class CommandTestCase(unittest.TestCase): 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): @@ -253,7 +255,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', '', From 757a8b300eb77cde1cc070b5586ae2635864409d Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sat, 5 Dec 2015 20:30:18 +0100 Subject: [PATCH 02/16] Add an update mechanism for old repositories The update is done transparently, and displays a warning message explaining the change. All the update machinery has been moved to the new update module. --- pubs/__init__.py | 2 +- pubs/commands/update_cmd.py | 39 ----------- pubs/config/__init__.py | 2 +- pubs/config/conf.py | 27 +++++--- pubs/pubs_cmd.py | 32 ++------- pubs/update.py | 63 ++++++++++++++++++ tests/test_config.py | 125 ++++++++++++++++++------------------ tests/test_databroker.py | 5 +- 8 files changed, 152 insertions(+), 143 deletions(-) delete mode 100644 pubs/commands/update_cmd.py create mode 100644 pubs/update.py 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/commands/update_cmd.py b/pubs/commands/update_cmd.py deleted file mode 100644 index 0457f6a..0000000 --- a/pubs/commands/update_cmd.py +++ /dev/null @@ -1,39 +0,0 @@ -import sys - -from .. import repo -from .. import color - -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(conf, args): - - ui = get_ui() - - code_version = __version__.split('.') - if len(conf['internal']['version']) == 1: # support for deprecated version scheme. - conf['internal']['version'] = '0.{}.0'.format(conf['internal']['version']) - repo_version = conf['internal']['version'].split('.') - - 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(conf['main']['pubsdir'], color.filepath)) - sure = ui.input_yn(question=msg, default='n') - if not sure: - sys.exit(0) - - #TODO: update!! -# conf['internal']['version'] = repo_version -# conf['internal']['version'] diff --git a/pubs/config/__init__.py b/pubs/config/__init__.py index d7ea4b7..e9d8caa 100644 --- a/pubs/config/__init__.py +++ b/pubs/config/__init__.py @@ -1 +1 @@ -from .conf import load_default_conf, load_conf, save_conf, get_pubspath +from .conf import get_confpath, load_default_conf, load_conf, save_conf, check_conf diff --git a/pubs/config/conf.py b/pubs/config/conf.py index b891fd3..7867955 100644 --- a/pubs/config/conf.py +++ b/pubs/config/conf.py @@ -15,7 +15,7 @@ def load_default_conf(): default_conf.validate(validator, copy=True) return default_conf -def get_pubspath(verify=True): +def get_confpath(verify=True): """Returns the pubs path. If verify is True, verify that pubs.conf exist in the directory, and exit with an error if not. @@ -31,19 +31,26 @@ def get_pubspath(verify=True): ui.exit(error_code=1) return confpath -def load_conf(check_conf=True): +def check_conf(conf): + """Type checks 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 user config""" - pubspath = get_pubspath(verify=True) - with open(pubspath, 'r') as f: + if path is None: + path = get_confpath(verify=True) + with open(path, 'rb') as f: conf = configobj.ConfigObj(f.readlines(), configspec=configspec) - if check_conf: - validator = validate.Validator() - results = conf.validate(validator, copy=True) - assert results == True, '{}'.format(results) # TODO: precise error dialog when parsing error + if check: + check_conf(conf) return conf -def save_conf(conf): - with open(get_pubspath(verify=False), 'w') as f: +def save_conf(conf, path=None): + if path is None: + path = get_confpath(verify=False) + with open(path, 'wb') as f: conf.write(outfile=f) diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index 16d7a80..512dc79 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -6,8 +6,8 @@ import collections from . import uis from . import config from . import commands +from . import update from . import plugins -from .__init__ import __version__ CORE_CMDS = collections.OrderedDict([ @@ -27,37 +27,17 @@ CORE_CMDS = collections.OrderedDict([ ('websearch', commands.websearch_cmd), ('edit', commands.edit_cmd), - # ('update', commands.update_cmd), ]) -def _update_check(conf, ui): - code_version = __version__.split('.') - if len(conf['internal']['version']) == 1: # support for deprecated version scheme. - conf['internal']['version'] = '0.{}.0'.format(conf['internal']['version']) - repo_version = conf['internal']['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.') - 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() - - def execute(raw_args=sys.argv): # loading config if len(raw_args) > 1 and raw_args[1] != 'init': try: - conf = config.load_conf(check_conf=True) + conf = config.load_conf(check=False) + if update.update_check(conf): # an update happened, reload conf. + conf = config.load_conf(check=False) + config.check_conf(conf) except IOError as e: print('error: {}'.format(str(e))) sys.exit() @@ -67,8 +47,6 @@ def execute(raw_args=sys.argv): uis.init_ui(conf) ui = uis.get_ui() - _update_check(conf, ui) - parser = argparse.ArgumentParser(description="research papers repository") subparsers = parser.add_subparsers(title="valid commands", dest="command") diff --git a/pubs/update.py b/pubs/update.py new file mode 100644 index 0000000..7f90832 --- /dev/null +++ b/pubs/update.py @@ -0,0 +1,63 @@ +from . import config +from . import uis +from .__init__ import __version__ + + +def update_check(conf): + """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) + + return False + +def update(conf, code_version, repo_version): + """Runs an update if necessary, and return True in that case.""" + + if repo_version == ['0', '5', '0']: # we need to update + default_conf = config.load_default_conf() + uis.init_ui(config.load_default_conf()) + ui = uis.get_ui() + + for key in ['pubsdir', 'docsdir', 'edit_cmd', 'open_cmd']: + default_conf['main'][key] = conf['pubs'][key] + if conf['pubs']['import_move']: + default_conf['main']['add_doc'] = 'move' + elif conf['pubs']['import_copy']: + default_conf['main']['add_doc'] = 'copy' + else: + default_conf['main']['add_doc'] = 'link' + + backup_path = config.get_confpath() + '.old' + config.save_conf(conf, path=backup_path) + config.save_conf(default_conf) + + ui.warning( + 'Your configuration file has been updated. ' + 'The old file has been moved to `{}`. '.format(backup_path) + + '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 + return False 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) From 14df0ad1cb6e103d4f61e605e8d2f6123fc1d607 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sat, 5 Dec 2015 21:18:57 +0100 Subject: [PATCH 03/16] Add pubs conf command Allows to edit the configuration file using pubs directly. Will check if the modified configuration file is valid. --- pubs/commands/__init__.py | 3 ++- pubs/commands/conf_cmd.py | 36 ++++++++++++++++++++++++++++++++++++ pubs/config/conf.py | 12 ++++++------ pubs/pubs_cmd.py | 2 ++ setup.py | 2 +- 5 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 pubs/commands/conf_cmd.py 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/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/config/conf.py b/pubs/config/conf.py index 7867955..259e7eb 100644 --- a/pubs/config/conf.py +++ b/pubs/config/conf.py @@ -9,16 +9,15 @@ from .spec import configspec DFT_CONFIG_PATH = os.path.expanduser('~/.pubsrc') def load_default_conf(): - """Loads the default configuration""" + """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): - """Returns the pubs path. - If verify is True, verify that pubs.conf exist in the directory, - and exit with an error if not. + """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: @@ -32,13 +31,13 @@ def get_confpath(verify=True): return confpath def check_conf(conf): - """Type checks a configuration""" + """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 user config""" + """Load the configuration""" if path is None: path = get_confpath(verify=True) with open(path, 'rb') as f: @@ -50,6 +49,7 @@ def load_conf(check=True, path=None): return conf def save_conf(conf, path=None): + """Save the configuration.""" if path is None: path = get_confpath(verify=False) with open(path, 'wb') as f: diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index 512dc79..3b2fc53 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -12,6 +12,8 @@ from . import plugins CORE_CMDS = collections.OrderedDict([ ('init', commands.init_cmd), + ('conf', commands.conf_cmd), + ('add', commands.add_cmd), ('rename', commands.rename_cmd), ('remove', commands.remove_cmd), diff --git a/setup.py b/setup.py index 145a58d..462d453 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', From 789db939110ed4f193729eb93f9e8ee26e7178eb Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 6 Dec 2015 08:41:33 +0100 Subject: [PATCH 04/16] Add platform-specific open and edit commands at init The main motivation is to provide sensible default to make the commnand "just work" without needing to fiddle with the configuration --- pubs/commands/init_cmd.py | 2 ++ pubs/config/__init__.py | 1 + pubs/config/conf.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/pubs/commands/init_cmd.py b/pubs/commands/init_cmd.py index 0bfaa0c..6329e0e 100644 --- a/pubs/commands/init_cmd.py +++ b/pubs/commands/init_cmd.py @@ -41,6 +41,8 @@ def command(conf, args): 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(conf, create=True) diff --git a/pubs/config/__init__.py b/pubs/config/__init__.py index e9d8caa..5de91fb 100644 --- a/pubs/config/__init__.py +++ b/pubs/config/__init__.py @@ -1 +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 index 259e7eb..aefeda2 100644 --- a/pubs/config/conf.py +++ b/pubs/config/conf.py @@ -1,4 +1,6 @@ import os +import platform +import shutil import configobj import validate @@ -54,3 +56,37 @@ def save_conf(conf, path=None): path = get_confpath(verify=False) with open(path, '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 From 3099d353f96706fc0912fa0eb96840b8d92bb2a5 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 7 Dec 2015 09:47:56 +0100 Subject: [PATCH 05/16] Configurable colors and update improvement Add a theme section in the configuration file to allow users to set the colors used by different elements of the ui. Improve the update mechanism so that incremental changes to the configuration file can be incorporated. --- pubs/color.py | 23 +++++++++------- pubs/commands/attach_cmd.py | 4 +-- pubs/commands/import_cmd.py | 2 +- pubs/commands/init_cmd.py | 4 +-- pubs/commands/open_cmd.py | 4 +-- pubs/commands/remove_cmd.py | 6 ++-- pubs/commands/tag_cmd.py | 5 ++-- pubs/config/spec.py | 33 ++++++++++++++++++---- pubs/pretty.py | 13 +++++---- pubs/uis.py | 10 ++----- pubs/update.py | 55 +++++++++++++++++++++++++++++++------ pubs/utils.py | 4 +-- tests/test_color.py | 2 +- tests/test_pretty.py | 5 ++-- 14 files changed, 115 insertions(+), 55 deletions(-) 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/attach_cmd.py b/pubs/commands/attach_cmd.py index 41bcdea..99bd10b 100644 --- a/pubs/commands/attach_cmd.py +++ b/pubs/commands/attach_cmd.py @@ -37,10 +37,10 @@ def command(conf, 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/import_cmd.py b/pubs/commands/import_cmd.py index 30d200e..ceda2bd 100644 --- a/pubs/commands/import_cmd.py +++ b/pubs/commands/import_cmd.py @@ -80,7 +80,7 @@ def command(conf, 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)) diff --git a/pubs/commands/init_cmd.py b/pubs/commands/init_cmd.py index 6329e0e..048e713 100644 --- a/pubs/commands/init_cmd.py +++ b/pubs/commands/init_cmd.py @@ -34,10 +34,10 @@ def command(conf, 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'))) conf['main']['pubsdir'] = pubsdir conf['main']['docsdir'] = docsdir diff --git a/pubs/commands/open_cmd.py b/pubs/commands/open_cmd.py index fa4cec0..226d236 100644 --- a/pubs/commands/open_cmd.py +++ b/pubs/commands/open_cmd.py @@ -33,7 +33,7 @@ def command(conf, args): 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: @@ -41,7 +41,7 @@ def command(conf, args): cmd = with_command.split() cmd.append(docpath) subprocess.Popen(cmd) - ui.message('{} opened.'.format(color.dye(docpath, color.filepath))) + ui.message('{} opened.'.format(color.dye(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 47e20d8..ec39f48 100644 --- a/pubs/commands/remove_cmd.py +++ b/pubs/commands/remove_cmd.py @@ -21,12 +21,12 @@ def command(conf, args): 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/tag_cmd.py b/pubs/commands/tag_cmd.py index fb8e9a2..29e1078 100644 --- a/pubs/commands/tag_cmd.py +++ b/pubs/commands/tag_cmd.py @@ -83,13 +83,12 @@ def command(conf, args): 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 == []: - 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_tags(tags)) for tag in add_tags: diff --git a/pubs/config/spec.py b/pubs/config/spec.py index 6f8be61..699a26d 100644 --- a/pubs/config/spec.py +++ b/pubs/config/spec.py @@ -13,10 +13,6 @@ docsdir = string(default="docsdir://") # linked when adding a publication. doc_add = option('copy', 'move', 'link', default='move') -# if True, pubs will ask confirmation before copying/moving/linking the -# document file. -doc_add_ask = boolean(default=True) - # the command to use when opening document files open_cmd = string(default=None) @@ -41,7 +37,34 @@ 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] 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/uis.py b/pubs/uis.py index 82e8744..ac566bf 100644 --- a/pubs/uis.py +++ b/pubs/uis.py @@ -44,9 +44,7 @@ class PrintUI(object): :param conf: if None, conservative default values are used. Useful to instanciate the UI before parsing the config file. """ - color.setup(color=conf['formating']['color'], - bold=conf['formating']['bold'], - italic=conf['formating']['italics']) + color.setup(conf) self.encoding = _get_encoding(conf) self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(), errors='replace') @@ -59,11 +57,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) @@ -97,14 +95,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 index 7f90832..71c582c 100644 --- a/pubs/update.py +++ b/pubs/update.py @@ -1,9 +1,13 @@ +import shutil +import StringIO + from . import config from . import uis +from . import color from .__init__ import __version__ -def update_check(conf): +def update_check(conf, path=None): """Runs an update if necessary, and return True in that case.""" code_version = __version__.split('.') @@ -24,18 +28,18 @@ def update_check(conf): 'newest version.') sys.exit() - elif repo_version < code_version: - return update(conf, code_version, repo_version) + elif repo_version <= code_version: + return update(conf, code_version, repo_version, path=path) return False -def update(conf, code_version, repo_version): +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() - uis.init_ui(config.load_default_conf()) - ui = uis.get_ui() for key in ['pubsdir', 'docsdir', 'edit_cmd', 'open_cmd']: default_conf['main'][key] = conf['pubs'][key] @@ -46,13 +50,15 @@ def update(conf, code_version, repo_version): else: default_conf['main']['add_doc'] = 'link' - backup_path = config.get_confpath() + '.old' - config.save_conf(conf, path=backup_path) + 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. ' - 'The old file has been moved to `{}`. '.format(backup_path) + + '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 ' @@ -60,4 +66,35 @@ def update(conf, code_version, repo_version): ) return True + + # continuous update while configuration is stabilizing + if repo_version == code_version == ['0', '6', '0']: + 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 + + # comparing potential changes + with open(path, 'r') as f: + old_conf_text = f.read() + new_conf_text = StringIO.StringIO() + default_conf.write(outfile=new_conf_text) + + if new_conf_text.getvalue() != old_conf_text: + + 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.\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/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_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() From 796087af450f234671d6ab528d785873f0b376a5 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Tue, 8 Dec 2015 23:51:44 +0100 Subject: [PATCH 06/16] Fix missing update of dye in open command Essentially reapply commit bd3659a432 that got swallowed in the merge. --- pubs/commands/open_cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubs/commands/open_cmd.py b/pubs/commands/open_cmd.py index 226d236..bee1378 100644 --- a/pubs/commands/open_cmd.py +++ b/pubs/commands/open_cmd.py @@ -41,7 +41,7 @@ def command(conf, args): cmd = with_command.split() cmd.append(docpath) subprocess.Popen(cmd) - ui.message('{} opened.'.format(color.dye(docpath, 'filepath'))) + ui.message('{} opened.'.format(color.dye_out(docpath, 'filepath'))) except OSError: ui.error("Command does not exist: %s." % with_command) ui.exit(127) From 0bfe921ad9ef6cedcc585dcf0e34a63ea27330a5 Mon Sep 17 00:00:00 2001 From: Olivier Mangin Date: Wed, 9 Dec 2015 10:56:01 -0500 Subject: [PATCH 07/16] Makes config updater robust ton undefined values. --- pubs/update.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pubs/update.py b/pubs/update.py index 96ca080..4795964 100644 --- a/pubs/update.py +++ b/pubs/update.py @@ -42,10 +42,11 @@ def update(conf, code_version, repo_version, path=None): default_conf = config.load_default_conf() for key in ['pubsdir', 'docsdir', 'edit_cmd', 'open_cmd']: - default_conf['main'][key] = conf['pubs'][key] - if conf['pubs']['import_move']: + 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['pubs']['import_copy']: + elif conf.get('import_copy'): default_conf['main']['add_doc'] = 'copy' else: default_conf['main']['add_doc'] = 'link' From 2871588703778e32993fc4d574f2cfa96f82c262 Mon Sep 17 00:00:00 2001 From: 73 Date: Mon, 7 Dec 2015 15:39:14 +0100 Subject: [PATCH 08/16] implements #19 --- pubs/config/conf.py | 20 ++++++++++---- pubs/pubs_cmd.py | 61 +++++++++++++++++++++++++------------------ pubs/uis.py | 3 ++- tests/test_usecase.py | 14 ++++++++++ 4 files changed, 67 insertions(+), 31 deletions(-) diff --git a/pubs/config/conf.py b/pubs/config/conf.py index aefeda2..6c41e21 100644 --- a/pubs/config/conf.py +++ b/pubs/config/conf.py @@ -2,6 +2,7 @@ import os import platform import shutil + import configobj import validate @@ -10,6 +11,7 @@ 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) @@ -17,6 +19,7 @@ def load_default_conf(): 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. @@ -32,31 +35,36 @@ def get_confpath(verify=True): 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 None: - path = get_confpath(verify=False) - with open(path, 'wb') as f: + 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': @@ -68,6 +76,7 @@ def default_open_cmd(): else: return None + def which(cmd): try: return shutil.which(cmd) # available in python 3.3 @@ -78,6 +87,7 @@ def which(cmd): return filepath return None + def default_edit_cmd(): """Find an available editor""" if 'EDITOR' in os.environ: diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index 3b2fc53..a98d629 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -1,57 +1,68 @@ import sys - import argparse import collections - from . import uis from . import config from . import commands from . import update from . import plugins - +from .__init__ import __version__ CORE_CMDS = collections.OrderedDict([ - ('init', commands.init_cmd), - ('conf', commands.conf_cmd), + ('init', commands.init_cmd), + ('conf', commands.conf_cmd), - ('add', commands.add_cmd), - ('rename', commands.rename_cmd), - ('remove', commands.remove_cmd), - ('list', commands.list_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), + ('attach', commands.attach_cmd), + ('open', commands.open_cmd), + ('tag', commands.tag_cmd), + ('note', commands.note_cmd), - ('export', commands.export_cmd), - ('import', commands.import_cmd), + ('export', commands.export_cmd), + ('import', commands.import_cmd), - ('websearch', commands.websearch_cmd), - ('edit', commands.edit_cmd), - ]) + ('websearch', commands.websearch_cmd), + ('edit', commands.edit_cmd), +]) def execute(raw_args=sys.argv): + + conf_parser = argparse.ArgumentParser(add_help=False) + conf_parser.add_argument("-c", "--config", help="path to config file", + type=str, metavar="FILE") + args, remaining_args = conf_parser.parse_known_args(raw_args[1:]) + + if args.config: + conf_path = args.config + else: + conf_path = config.get_confpath(verify=False) # will be checked on load + # loading config - if len(raw_args) > 1 and raw_args[1] != 'init': + if len(remaining_args) > 0 and remaining_args[0] != 'init': try: - conf = config.load_conf(check=False) - if update.update_check(conf): # an update happened, reload conf. - conf = config.load_conf(check=False) + 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() else: conf = config.load_default_conf() + conf.filename = conf_path uis.init_ui(conf) ui = uis.get_ui() - parser = argparse.ArgumentParser(description="research papers repository") + parser = argparse.ArgumentParser(parents=[conf_parser], + description="research papers repository", + prog="pubs", version=__version__, add_help=True) subparsers = parser.add_subparsers(title="valid commands", dest="command") - cmd_funcs = collections.OrderedDict() for cmd_name, cmd_mod in CORE_CMDS.items(): cmd_mod.parser(subparsers) @@ -62,7 +73,7 @@ def execute(raw_args=sys.argv): for p in plugins.get_plugins().values(): cmd_funcs.update(p.get_commands(subparsers)) - args = parser.parse_args(raw_args[1:]) + args = parser.parse_args(remaining_args) args.prog = parser.prog # Hack: there might be a better way... cmd = args.command del args.command diff --git a/pubs/uis.py b/pubs/uis.py index c0c472d..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 diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 9ec2b3e..1dd9f7d 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -59,6 +59,7 @@ class CommandTestCase(unittest.TestCase): def setUp(self): 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 @@ -509,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() From 4b21fa1355ab9a86f1ae9d2a647738a1c83ed27c Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Thu, 10 Dec 2015 21:50:18 +0100 Subject: [PATCH 09/16] Fix handling of version in argument parser --- pubs/pubs_cmd.py | 6 +++--- pubs/update.py | 2 ++ tests/test_usecase.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index a98d629..a4854a2 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -59,9 +59,9 @@ def execute(raw_args=sys.argv): uis.init_ui(conf) ui = uis.get_ui() - parser = argparse.ArgumentParser(parents=[conf_parser], - description="research papers repository", - prog="pubs", version=__version__, add_help=True) + 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") cmd_funcs = collections.OrderedDict() for cmd_name, cmd_mod in CORE_CMDS.items(): diff --git a/pubs/update.py b/pubs/update.py index 4795964..2a31aba 100644 --- a/pubs/update.py +++ b/pubs/update.py @@ -85,10 +85,12 @@ def update(conf, code_version, repo_version, path=None): 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) diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 1dd9f7d..4f55c04 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -18,7 +18,7 @@ from pubs.commands import init_cmd, import_cmd # makes the tests very noisy -messagePUT=False +PRINT_OUTPUT=False CAPTURE_OUTPUT=True @@ -104,7 +104,7 @@ 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 From eaa6ca8dab192467b2f07a373a777d6b05357f13 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Thu, 10 Dec 2015 22:14:38 +0100 Subject: [PATCH 10/16] Proper error message when no subcommand is provided On Python 3, invoking `pubs` would display a stacktrace, because the subparser was not required. This commit also simplifies the parser code by using .set_default() to specify which command should be run. This prompted changes in the plugin code. The code is still not functioning, but many fixes in this commit bring the code closer to a working state. --- pubs/plugins.py | 6 +++--- pubs/plugs/alias/alias.py | 19 ++++++++++--------- pubs/pubs_cmd.py | 21 ++++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) 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..131b716 100644 --- a/pubs/plugs/alias/alias.py +++ b/pubs/plugs/alias/alias.py @@ -18,7 +18,7 @@ class Alias(object): p.add_argument('arguments', nargs='*', help="arguments to be passed to the user defined command") - 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,13 @@ class AliasPlugin(PapersPlugin): name = 'alias' - def __init__(self): + def __init__(self, conf): self.aliases = [] - for name, definition in config('alias').items(): + for name, definition in conf['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] + def update_parser(self, subparsers): + """Add subcommand to the provided subparser""" + for alias in self.aliases: + alias_parser = alias.parser(parser) + alias_parser.set_defaults(func=alias.command) diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index a4854a2..0f94e50 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -42,7 +42,7 @@ def execute(raw_args=sys.argv): else: conf_path = config.get_confpath(verify=False) # will be checked on load - # loading config + # Loading config if len(remaining_args) > 0 and remaining_args[0] != 'init': try: conf = config.load_conf(path=conf_path, check=False) @@ -63,19 +63,18 @@ def execute(raw_args=sys.argv): prog="pubs", add_help=True) parser.add_argument('--version', action='version', version=__version__) subparsers = parser.add_subparsers(title="valid commands", dest="command") - cmd_funcs = collections.OrderedDict() + subparsers.required = True + + # 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, conf['plugins']['active']) + plugins.load_plugins(conf, ui) for p in plugins.get_plugins().values(): - cmd_funcs.update(p.get_commands(subparsers)) + p.update_parser(subparsers) + # Parse and run appropriate command args = parser.parse_args(remaining_args) - args.prog = parser.prog # Hack: there might be a better way... - cmd = args.command - del args.command - - cmd_funcs[cmd](conf, args) + args.func(conf, args) From 414043e88e7b8cedfb7684cdf9c8c20d10a0f349 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Fri, 11 Dec 2015 14:35:49 +0100 Subject: [PATCH 11/16] Add Arnold Sykosch to authors --- readme.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/readme.md b/readme.md index f741495..cc0aaa1 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 @@ -63,17 +63,17 @@ Pubs is designed to interact well with your command line tool chain. You can add For more advanced functionalities, pubs also support plugins. Actually *alias* is itself a plugin! -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 From af207e260421c37db2abcad6722fbc12f4317e92 Mon Sep 17 00:00:00 2001 From: Olivier Mangin Date: Fri, 11 Dec 2015 20:11:38 -0500 Subject: [PATCH 12/16] Adds alias plugin test and fix its config access. --- pubs/plugs/alias/alias.py | 8 ++-- tests/test_plug_alias.py | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 tests/test_plug_alias.py diff --git a/pubs/plugs/alias/alias.py b/pubs/plugs/alias/alias.py index 131b716..1543717 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,7 +15,7 @@ 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") def command(self, conf, args): raise NotImplementedError @@ -61,8 +60,9 @@ class AliasPlugin(PapersPlugin): def __init__(self, conf): self.aliases = [] - for name, definition in conf['alias'].items(): - self.aliases.append(Alias.create_alias(name, definition)) + 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""" 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) From 16aa0163f641ccfccb1aace59ab9ad9af1085e30 Mon Sep 17 00:00:00 2001 From: Olivier Mangin Date: Fri, 11 Dec 2015 21:33:54 -0500 Subject: [PATCH 13/16] Removes automatic config update. Makes it optional through command switch. --- pubs/pubs_cmd.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index 0f94e50..3193b61 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -8,6 +8,7 @@ from . import update from . import plugins from .__init__ import __version__ + CORE_CMDS = collections.OrderedDict([ ('init', commands.init_cmd), ('conf', commands.conf_cmd), @@ -35,6 +36,8 @@ def execute(raw_args=sys.argv): conf_parser = argparse.ArgumentParser(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') args, remaining_args = conf_parser.parse_known_args(raw_args[1:]) if args.config: @@ -46,7 +49,8 @@ def execute(raw_args=sys.argv): if len(remaining_args) > 0 and remaining_args[0] != 'init': try: conf = config.load_conf(path=conf_path, check=False) - if update.update_check(conf, path=conf.filename): # an update happened, reload conf. + if args.update and 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: From ba48941d9377138733fed7abe902fedadbed263e Mon Sep 17 00:00:00 2001 From: Olivier Mangin Date: Fri, 11 Dec 2015 21:34:28 -0500 Subject: [PATCH 14/16] Fixes alias plugins and adds it to packages. --- pubs/plugs/alias/alias.py | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pubs/plugs/alias/alias.py b/pubs/plugs/alias/alias.py index 1543717..eebf1f9 100644 --- a/pubs/plugs/alias/alias.py +++ b/pubs/plugs/alias/alias.py @@ -16,6 +16,7 @@ class Alias(object): 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") + return p def command(self, conf, args): raise NotImplementedError @@ -67,5 +68,5 @@ class AliasPlugin(PapersPlugin): def update_parser(self, subparsers): """Add subcommand to the provided subparser""" for alias in self.aliases: - alias_parser = alias.parser(parser) + alias_parser = alias.parser(subparsers) alias_parser.set_defaults(func=alias.command) diff --git a/setup.py b/setup.py index 462d453..7f2525d 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( packages = ['pubs', 'pubs.config', 'pubs.commands', 'pubs.templates', - 'pubs.plugs'], + 'pubs.plugs', 'pubs.plugs.alias'], scripts = ['pubs/pubs'], install_requires = ['pyyaml', 'bibtexparser', 'python-dateutil', 'requests', 'configobj', From 7186576aef3ae67639ebd483e291b3d3e5a24a6a Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sat, 12 Dec 2015 15:02:59 +0100 Subject: [PATCH 15/16] Fix bug in args & less-aggressive update Without args.prog, AliasCommand fail. Alias is better documented in the readme and initial config files. Update will now only trigger with version bumps. And the plugins section is not updated. Removes the --upgrade flag. --- pubs/config/spec.py | 8 ++++++++ pubs/pubs_cmd.py | 15 ++++++++------- pubs/update.py | 7 +++++-- readme.md | 14 +++++++------- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/pubs/config/spec.py b/pubs/config/spec.py index 699a26d..27cf449 100644 --- a/pubs/config/spec.py +++ b/pubs/config/spec.py @@ -71,6 +71,14 @@ pages = string(default='') # 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='{}') diff --git a/pubs/pubs_cmd.py b/pubs/pubs_cmd.py index 3193b61..b791234 100644 --- a/pubs/pubs_cmd.py +++ b/pubs/pubs_cmd.py @@ -33,15 +33,15 @@ CORE_CMDS = collections.OrderedDict([ def execute(raw_args=sys.argv): - conf_parser = argparse.ArgumentParser(add_help=False) + 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') - args, remaining_args = conf_parser.parse_known_args(raw_args[1:]) + #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 args.config: - conf_path = args.config + if top_args.config: + conf_path = top_args.config else: conf_path = config.get_confpath(verify=False) # will be checked on load @@ -49,7 +49,7 @@ def execute(raw_args=sys.argv): if len(remaining_args) > 0 and remaining_args[0] != 'init': try: conf = config.load_conf(path=conf_path, check=False) - if args.update and update.update_check(conf, path=conf.filename): + 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) @@ -81,4 +81,5 @@ def execute(raw_args=sys.argv): # Parse and run appropriate command args = parser.parse_args(remaining_args) + args.prog = "pubs" # FIXME? args.func(conf, args) diff --git a/pubs/update.py b/pubs/update.py index 2a31aba..aab8879 100644 --- a/pubs/update.py +++ b/pubs/update.py @@ -69,7 +69,7 @@ def update(conf, code_version, repo_version, path=None): return True # continuous update while configuration is stabilizing - if repo_version == code_version == ['0', '6', '0']: + 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(): @@ -78,6 +78,10 @@ def update(conf, code_version, repo_version, path=None): 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: @@ -85,7 +89,6 @@ def update(conf, code_version, repo_version, path=None): new_conf_text = io.BytesIO() default_conf.write(outfile=new_conf_text) - if new_conf_text.getvalue() != old_conf_text: backup_path = path + '.old' diff --git a/readme.md b/readme.md index cc0aaa1..9409f46 100644 --- a/readme.md +++ b/readme.md @@ -52,16 +52,16 @@ 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 - [alias] - print = open -w lp - count = !pubs list -k | wc -l +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. -For more advanced functionalities, pubs also support plugins. Actually *alias* is itself a plugin! + [[alias]] + evince = open --with evince + count = !pubs list -k | wc -l +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 From f52a7202bbe7dcb8b7831ce7e55e0ac826eff20a Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sat, 12 Dec 2015 15:08:22 +0100 Subject: [PATCH 16/16] Better help for the list command --- pubs/commands/list_cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubs/commands/list_cmd.py b/pubs/commands/list_cmd.py index a382672..9e7063b 100644 --- a/pubs/commands/list_cmd.py +++ b/pubs/commands/list_cmd.py @@ -27,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