Merge pull request #37 from pubs/feat/new_config

feat/new_config: better, more robust, more flexible configuration
main
Fabien Benureau 9 years ago
commit 76be98a900

@ -1 +1 @@
__version__ = '0.5.0' __version__ = '0.6.0'

@ -27,6 +27,7 @@ def generate_colors(stream, color=True, bold=True, italic=True):
colors[u'bold'] = u'' colors[u'bold'] = u''
colors[u'italic'] = u'' colors[u'italic'] = u''
colors[u'end'] = u'' colors[u'end'] = u''
colors[u''] = u''
if (color or bold or italic) and _color_supported(stream): if (color or bold or italic) and _color_supported(stream):
bold_flag, italic_flag = '', '' bold_flag, italic_flag = '', ''
@ -36,6 +37,8 @@ def generate_colors(stream, color=True, bold=True, italic=True):
if italic: if italic:
colors['italic'] = u'\033[3m' colors['italic'] = u'\033[3m'
italic_flag = '3;' italic_flag = '3;'
if bold and italic:
colors['bolditalic'] = u'\033[1;3m'
for i, name in enumerate(COLOR_LIST): for i, name in enumerate(COLOR_LIST):
if color: if color:
@ -69,10 +72,17 @@ def dye_err(s, color='end'):
def _nodye(s, *args, **kwargs): def _nodye(s, *args, **kwargs):
return s return s
def setup(color=False, bold=False, italic=False): def setup(conf):
global COLORS_OUT, COLORS_ERR global COLORS_OUT, COLORS_ERR
COLORS_OUT = generate_colors(sys.stdout, color=color, bold=bold, italic=italic) COLORS_OUT = generate_colors(sys.stdout, color=conf['formating']['color'],
COLORS_ERR = generate_colors(sys.stderr, color=color, bold=bold, italic=italic) 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
undye_re = re.compile('\x1b\[[;\d]*[A-Za-z]') 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): def undye(s):
"""Purge string s of color""" """Purge string s of color"""
return undye_re.sub('', s) return undye_re.sub('', s)
# colors
ok = 'green'
error = 'red'
citekey = 'purple'
filepath = 'bold'
tag = 'cyan'

@ -1,5 +1,7 @@
# core # core
from . import init_cmd from . import init_cmd
from . import conf_cmd
from . import add_cmd from . import add_cmd
from . import rename_cmd from . import rename_cmd
from . import remove_cmd from . import remove_cmd
@ -16,4 +18,3 @@ from . import import_cmd
from . import websearch_cmd from . import websearch_cmd
from . import edit_cmd from . import edit_cmd
# from . import update_cmd

@ -1,5 +1,4 @@
from ..uis import get_ui from ..uis import get_ui
from ..configs import config
from .. import bibstruct from .. import bibstruct
from .. import content from .. import content
from .. import repo from .. import repo
@ -28,12 +27,12 @@ def parser(subparsers):
return parser return parser
def bibentry_from_editor(ui, rp): def bibentry_from_editor(conf, ui, rp):
again = True again = True
bibstr = templates.add_bib bibstr = templates.add_bib
while again: while again:
try: try:
bibstr = content.editor_input(config().edit_cmd, bibstr = content.editor_input(conf['main']['edit_cmd'],
bibstr, bibstr,
suffix='.bib') suffix='.bib')
if bibstr == templates.add_bib: if bibstr == templates.add_bib:
@ -57,7 +56,7 @@ def bibentry_from_editor(ui, rp):
return bibentry return bibentry
def command(args): def command(conf, args):
""" """
:param bibfile: bibtex file (in .bib, .bibml or .yaml format. :param bibfile: bibtex file (in .bib, .bibml or .yaml format.
:param docfile: path (no url yet) to a pdf or ps file :param docfile: path (no url yet) to a pdf or ps file
@ -69,12 +68,12 @@ def command(args):
tags = args.tags tags = args.tags
citekey = args.citekey citekey = args.citekey
rp = repo.Repository(config()) rp = repo.Repository(conf)
# get bibtex entry # get bibtex entry
if bibfile is None: if bibfile is None:
if args.doi is None and args.isbn 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: else:
if args.doi is not None: if args.doi is not None:
bibentry_raw = apis.doi2bibtex(args.doi) bibentry_raw = apis.doi2bibtex(args.doi)
@ -124,18 +123,28 @@ def command(args):
'{}, using {} instead.').format(bib_docfile, docfile)) '{}, using {} instead.').format(bib_docfile, docfile))
# create the paper # 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: try:
rp.push_paper(p) rp.push_paper(p)
if docfile is not None: if docfile is not None:
rp.push_doc(p.citekey, docfile, copy=args.copy or args.move) rp.push_doc(p.citekey, docfile, copy=copy or args.move)
if args.copy: if copy:
if args.move: if move:
content.remove_file(docfile) 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))) 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: except ValueError as v:
ui.error(v.message) ui.error(v.message)
ui.exit(1) ui.exit(1)

@ -1,6 +1,5 @@
from .. import repo from .. import repo
from .. import color from .. import color
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
from .. import content from .. import content
@ -20,7 +19,7 @@ def parser(subparsers):
return parser return parser
def command(args): def command(conf, args):
""" """
:param bibfile: bibtex file (in .bib, .bibml or .yaml format. :param bibfile: bibtex file (in .bib, .bibml or .yaml format.
:param docfile: path (no url yet) to a pdf or ps file :param docfile: path (no url yet) to a pdf or ps file
@ -28,7 +27,7 @@ def command(args):
ui = get_ui() ui = get_ui()
rp = repo.Repository(config()) rp = repo.Repository(conf)
paper = rp.pull_paper(args.citekey) paper = rp.pull_paper(args.citekey)
try: try:
@ -38,10 +37,10 @@ def command(args):
if args.move: if args.move:
content.remove_file(document) content.remove_file(document)
# else: # 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) # 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: except ValueError as v:
ui.error(v.message) ui.error(v.message)

@ -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

@ -1,6 +1,6 @@
from ..paper import Paper from ..paper import Paper
from .. import repo from .. import repo
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
from ..endecoder import EnDecoder from ..endecoder import EnDecoder
from ..utils import resolve_citekey from ..utils import resolve_citekey
@ -16,12 +16,12 @@ def parser(subparsers):
return parser return parser
def command(args): def command(conf, args):
ui = get_ui() ui = get_ui()
meta = args.meta meta = args.meta
rp = repo.Repository(config()) rp = repo.Repository(conf)
citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True) citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True)
paper = rp.pull_paper(citekey) paper = rp.pull_paper(citekey)

@ -1,7 +1,6 @@
from __future__ import print_function from __future__ import print_function
from .. import repo from .. import repo
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
from .. import endecoder from .. import endecoder
@ -14,14 +13,14 @@ def parser(subparsers):
return parser return parser
def command(args): def command(conf, args):
""" """
""" """
# :param bib_format (only 'bibtex' now) # :param bib_format (only 'bibtex' now)
ui = get_ui() ui = get_ui()
rp = repo.Repository(config()) rp = repo.Repository(conf)
try: try:
papers = [rp.pull_paper(c) for c in args.citekeys] papers = [rp.pull_paper(c) for c in args.citekeys]

@ -6,7 +6,7 @@ from .. import endecoder
from .. import bibstruct from .. import bibstruct
from .. import color from .. import color
from ..paper import Paper from ..paper import Paper
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
from ..content import system_path, read_file from ..content import system_path, read_file
@ -58,7 +58,7 @@ def many_from_path(bibpath):
return papers return papers
def command(args): def command(conf, args):
""" """
:param bibpath: path (no url yet) to a bibliography file :param bibpath: path (no url yet) to a bibliography file
""" """
@ -66,10 +66,10 @@ def command(args):
ui = get_ui() ui = get_ui()
bibpath = args.bibpath bibpath = args.bibpath
copy = args.copy copy = args.copy
if copy is None: if copy is None:
copy = config().import_copy copy = conf['main']['doc_add'] in ('copy', 'move')
rp = repo.Repository(config())
rp = repo.Repository(conf)
# Extract papers from bib # Extract papers from bib
papers = many_from_path(bibpath) papers = many_from_path(bibpath)
keys = args.keys or papers.keys() keys = args.keys or papers.keys()
@ -80,12 +80,13 @@ def command(args):
ui.error('could not load entry for citekey {}.'.format(k)) ui.error('could not load entry for citekey {}.'.format(k))
else: else:
rp.push_paper(p) 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) docfile = bibstruct.extract_docfile(p.bibdata)
if docfile is None: if docfile is None:
ui.warning("no file for {}.".format(p.citekey)) ui.warning("no file for {}.".format(p.citekey))
else: 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: except KeyError:
ui.error('no entry found for citekey {}.'.format(k)) ui.error('no entry found for citekey {}.'.format(k))
except IOError as e: except IOError as e:

@ -2,25 +2,25 @@
import os import os
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
from .. import color from .. import color
from ..repo import Repository from ..repo import Repository
from ..content import system_path, check_directory from ..content import system_path, check_directory
from .. import config
def parser(subparsers): def parser(subparsers):
parser = subparsers.add_parser('init', parser = subparsers.add_parser('init',
help="initialize the pubs directory") help="initialize the pubs directory")
parser.add_argument('-p', '--pubsdir', default=None, 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://', parser.add_argument('-d', '--docsdir', default='docsdir://',
help=('path to document directory (if not specified, documents will' help=('path to document directory (if not specified, documents will'
'be stored in /path/to/pubsdir/doc/)')) 'be stored in /path/to/pubsdir/doc/)'))
return parser return parser
def command(args): def command(conf, args):
"""Create a .pubs directory""" """Create a .pubs directory"""
ui = get_ui() ui = get_ui()
@ -34,13 +34,15 @@ def command(args):
if check_directory(pubsdir, fail=False) and len(os.listdir(pubsdir)) > 0: if check_directory(pubsdir, fail=False) and len(os.listdir(pubsdir)) > 0:
ui.error('directory {} is not empty.'.format( ui.error('directory {} is not empty.'.format(
color.dye_err(pubsdir, color.filepath))) color.dye_err(pubsdir, 'filepath')))
ui.exit() ui.exit()
ui.message('Initializing pubs in {}'.format(color.dye_out(pubsdir, color.filepath))) ui.message('Initializing pubs in {}'.format(color.dye_out(pubsdir, 'filepath')))
config().pubsdir = pubsdir conf['main']['pubsdir'] = pubsdir
config().docsdir = docsdir conf['main']['docsdir'] = docsdir
config().save() conf['main']['open_cmd'] = config.default_open_cmd()
conf['main']['edit_cmd'] = config.default_edit_cmd()
config.save_conf(conf)
Repository(config(), create=True) Repository(conf, create=True)

@ -3,7 +3,6 @@ from datetime import datetime
from .. import repo from .. import repo
from .. import pretty from .. import pretty
from .. import bibstruct from .. import bibstruct
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
@ -28,7 +27,7 @@ def parser(subparsers):
help='list only pubs without attached documents.') help='list only pubs without attached documents.')
parser.add_argument('query', nargs='*', 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 return parser
@ -36,9 +35,9 @@ def date_added(p):
return p.added or datetime(1, 1, 1) return p.added or datetime(1, 1, 1)
def command(args): def command(conf, args):
ui = get_ui() ui = get_ui()
rp = repo.Repository(config()) rp = repo.Repository(conf)
papers = filter(lambda p: filter_paper(p, args.query, papers = filter(lambda p: filter_paper(p, args.query,
case_sensitive=args.case_sensitive), case_sensitive=args.case_sensitive),
rp.all_papers()) rp.all_papers())

@ -1,8 +1,8 @@
from .. import repo from .. import repo
from .. import content from .. import content
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
def parser(subparsers): def parser(subparsers):
parser = subparsers.add_parser('note', parser = subparsers.add_parser('note',
help='edit the note attached to a paper') help='edit the note attached to a paper')
@ -11,17 +11,16 @@ def parser(subparsers):
return parser return parser
def command(args): def command(conf, args):
""" """
""" """
ui = get_ui() ui = get_ui()
rp = repo.Repository(conf)
rp = repo.Repository(config())
if not rp.databroker.exists(args.citekey): if not rp.databroker.exists(args.citekey):
ui.error("citekey {} not found".format(args.citekey)) ui.error("citekey {} not found".format(args.citekey))
ui.exit(1) ui.exit(1)
notepath = rp.databroker.real_notepath(args.citekey) 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)

@ -1,7 +1,7 @@
import subprocess import subprocess
from .. import repo from .. import repo
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
from .. import color from .. import color
from ..content import system_path from ..content import system_path
@ -17,21 +17,23 @@ def parser(subparsers):
return parser return parser
def command(args): def command(conf, args):
ui = get_ui() ui = get_ui()
with_command = args.with_command 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) citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True)
paper = rp.pull_paper(citekey) paper = rp.pull_paper(citekey)
if with_command is None: 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: if paper.docpath is None:
ui.error('No document associated with the entry {}.'.format( ui.error('No document associated with the entry {}.'.format(
color.dye_err(citekey, color.citekey))) color.dye_err(citekey, 'citekey')))
ui.exit() ui.exit()
try: try:
@ -39,7 +41,7 @@ def command(args):
cmd = with_command.split() cmd = with_command.split()
cmd.append(docpath) cmd.append(docpath)
subprocess.Popen(cmd) subprocess.Popen(cmd)
ui.message('{} opened.'.format(color.dye_out(docpath, color.filepath))) ui.message('{} opened.'.format(color.dye_out(docpath, 'filepath')))
except OSError: except OSError:
ui.error("Command does not exist: %s." % with_command) ui.error("Command does not exist: %s." % with_command)
ui.exit(127) ui.exit(127)

@ -1,6 +1,5 @@
from .. import repo from .. import repo
from .. import color from .. import color
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
@ -13,21 +12,21 @@ def parser(subparsers):
return parser return parser
def command(args): def command(conf, args):
ui = get_ui() ui = get_ui()
force = args.force force = args.force
rp = repo.Repository(config()) rp = repo.Repository(conf)
if force is None: if force is None:
are_you_sure = (("Are you sure you want to delete paper(s) [{}]" are_you_sure = (("Are you sure you want to delete paper(s) [{}]"
" (this will also delete associated documents)?") " (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') sure = ui.input_yn(question=are_you_sure, default='n')
if force or sure: if force or sure:
for c in args.citekeys: for c in args.citekeys:
rp.remove_paper(c) 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. # FIXME: print should check that removal proceeded well.
else: 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])))

@ -1,5 +1,4 @@
from ..uis import get_ui from ..uis import get_ui
from ..configs import config
from .. import bibstruct from .. import bibstruct
from .. import content from .. import content
from .. import repo from .. import repo
@ -14,14 +13,14 @@ def parser(subparsers):
return parser return parser
def command(args): def command(conf, args):
""" """
:param bibfile: bibtex file (in .bib, .bibml or .yaml format. :param bibfile: bibtex file (in .bib, .bibml or .yaml format.
:param docfile: path (no url yet) to a pdf or ps file :param docfile: path (no url yet) to a pdf or ps file
""" """
ui = get_ui() ui = get_ui()
rp = repo.Repository(config()) rp = repo.Repository(conf)
paper = rp.pull_paper(args.citekey) paper = rp.pull_paper(args.citekey)
rp.rename_paper(paper, args.new_citekey) rp.rename_paper(paper, args.new_citekey)

@ -20,7 +20,6 @@ The different use cases are :
import re import re
from ..repo import Repository from ..repo import Repository
from ..configs import config
from ..uis import get_ui from ..uis import get_ui
from .. import pretty from .. import pretty
from .. import color from .. import color
@ -70,23 +69,22 @@ def _tag_groups(tags):
minus_tags.append(tag[1:]) minus_tags.append(tag[1:])
return set(plus_tags), set(minus_tags) return set(plus_tags), set(minus_tags)
def command(conf, args):
def command(args):
"""Add, remove and show tags""" """Add, remove and show tags"""
ui = get_ui() ui = get_ui()
citekeyOrTag = args.citekeyOrTag citekeyOrTag = args.citekeyOrTag
tags = args.tags tags = args.tags
rp = Repository(config()) rp = Repository(conf)
if citekeyOrTag is None: 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: else:
if rp.databroker.exists(citekeyOrTag): if rp.databroker.exists(citekeyOrTag):
p = rp.pull_paper(citekeyOrTag) p = rp.pull_paper(citekeyOrTag)
if tags is None: if tags is None:
ui.message(color.dye_out(' '.join(sorted(p.tags)), color.tag)) ui.message(color.dye_out(' '.join(sorted(p.tags)), 'tag'))
else: else:
add_tags, remove_tags = _tag_groups(_parse_tag_seq(tags)) add_tags, remove_tags = _tag_groups(_parse_tag_seq(tags))
for tag in add_tags: for tag in add_tags:

@ -1,37 +0,0 @@
import sys
from .. import repo
from .. import color
from ..configs import config
from ..uis import get_ui
from ..__init__ import __version__
def parser(subparsers):
parser = subparsers.add_parser('update', help='update the repository to the lastest format')
return parser
def command(args):
ui = get_ui()
code_version = __version__
repo_version = int(config().version)
if repo_version == code_version:
ui.message('Your pubs repository is up-to-date.')
sys.exit(0)
elif repo_version > code_version:
ui.message('Your repository was generated with an newer version of pubs.\n'
'You should not use pubs until you install the newest version.')
sys.exit(0)
else:
msg = ("You should backup the pubs directory {} before continuing."
"Continue ?").format(color.dye_out(config().papers_dir, color.filepath))
sure = ui.input_yn(question=msg, default='n')
if not sure:
sys.exit(0)
# config().version = repo_version
# config().save()

@ -11,7 +11,7 @@ def parser(subparsers):
return parser return parser
def command(args): def command(conf, args):
ui = get_ui() ui = get_ui()
search_string = args.search_string search_string = args.search_string

@ -0,0 +1,2 @@
from .conf import get_confpath, load_default_conf, load_conf, save_conf, check_conf
from .conf import default_open_cmd, default_edit_cmd

@ -0,0 +1,102 @@
import os
import platform
import shutil
import configobj
import validate
from .spec import configspec
DFT_CONFIG_PATH = os.path.expanduser('~/.pubsrc')
def load_default_conf():
"""Load the default configuration"""
default_conf = configobj.ConfigObj(configspec=configspec)
validator = validate.Validator()
default_conf.validate(validator, copy=True)
return default_conf
def get_confpath(verify=True):
"""Return the configuration filepath
If verify is True, verify that the file exists and exit with an error if not.
"""
confpath = DFT_CONFIG_PATH
if 'PUBSCONF' in os.environ:
confpath = os.path.abspath(os.path.expanduser(os.environ['PUBSCONF']))
if verify:
if not os.path.isfile(confpath):
from .. import uis
ui = uis.get_ui()
ui.error('configuration file not found at `{}`'.format(confpath))
ui.exit(error_code=1)
return confpath
def check_conf(conf):
"""Type check a configuration"""
validator = validate.Validator()
results = conf.validate(validator, copy=True)
assert results == True, '{}'.format(results) # TODO: precise error dialog when parsing error
def load_conf(check=True, path=None):
"""Load the configuration"""
if path is None:
path = get_confpath(verify=True)
with open(path, 'rb') as f:
conf = configobj.ConfigObj(f.readlines(), configspec=configspec)
if check:
check_conf(conf)
conf.filename = path
return conf
def save_conf(conf, path=None):
"""Save the configuration."""
if path is not None:
conf.filename = path
elif conf.filename is None:
conf.filename = get_confpath(verify=False)
with open(conf.filename, 'wb') as f:
conf.write(outfile=f)
def default_open_cmd():
"""Chooses the default command to open documents"""
if platform.system() == 'Darwin':
return 'open'
elif platform.system() == 'Linux':
return 'xdg-open'
elif platform.system() == 'Windows':
return 'start'
else:
return None
def which(cmd):
try:
return shutil.which(cmd) # available in python 3.3
except AttributeError:
for path in ['.'] + os.environ["PATH"].split(os.pathsep):
filepath = os.path.join(path.strip('"'), cmd)
if os.path.isfile(path) and os.access(path, os.X_OK):
return filepath
return None
def default_edit_cmd():
"""Find an available editor"""
if 'EDITOR' in os.environ:
return os.environ['EDITOR']
elif platform.system() == 'Darwin' or 'Linux':
for editor in ['vim', 'nano', 'emacs', 'vi']:
if which(editor) is not None:
return editor
elif platform.system() == 'Windows':
return 'Notepad.exe | Out-Null' # wait for notepad to close
else:
return None

@ -0,0 +1,86 @@
from .. import __version__
configspec = """
[main]
# Where the pubs repository files (bibtex, metadata, notes) are located
pubsdir = string(default='~/pubs')
# Where the documents files are located (default: $(pubsdir)/doc/)
docsdir = string(default="docsdir://")
# Specify if a document should be copied or moved in the docdir, or only
# linked when adding a publication.
doc_add = option('copy', 'move', 'link', default='move')
# the command to use when opening document files
open_cmd = string(default=None)
# which editor to use when editing bibtex files.
# if using a graphical editor, use the --wait or --block option, i.e.:
# "atom --wait"
# "kate --block"
edit_cmd = string(default=None)
[formating]
# Enable bold formatting, if the terminal supports it.
bold = boolean(default=True)
# Enable italics, if the terminal supports it.
italics = boolean(default=True)
# Enable colors, if the terminal supports it.
color = boolean(default=True)
[theme]
# Here you can define the color theme used by pubs, if enabled in the
# 'formating' section. Predefined theme are available at:
# https://github.com/pubs/pubs/blob/master/theme.md
# Available colors are: 'black', 'red', 'green', 'yellow', 'blue', 'purple',
# 'cyan', and 'grey'. Bold colors are available by prefixing 'b' in front of
# the color name ('bblack', 'bred', etc.), italic colors by prefixing 'i',
# and bold italic by prefixing 'bi'. Finally, 'bold', 'italic' and
# 'bolditalic' can be used to apply formatting without changing the color.
# For no color, use an empty string ''
# messages
ok = string(default='green')
warning = string(default='yellow')
error = string(default='red')
# ui elements
filepath = string(default='bold')
citekey = string(default='purple')
tag = string(default='cyan')
# bibliographic fields
author = string(default='bold')
title = string(default='')
publisher = string(default='italic')
year = string(default='bold')
volume = string(default='bold')
pages = string(default='')
[plugins]
# comma-separated list of the plugins to load
active = list(default=list())
[[alias]]
# new subcommands can be defined, e.g.:
# print = open --with lp
# evince = open --with evince
# shell commands can also be defined, by prefixing them with a bang `!`, e.g:
# count = !pubs list -k | wc -l
[internal]
# The version of this configuration file. Do not edit.
version = string(min=5, default='{}')
""".format(__version__).split('\n')

@ -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')

@ -28,13 +28,13 @@ class PapersPlugin(object):
raise RuntimeError("{} instance not created".format(cls.__name__)) 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 """Imports the modules for a sequence of plugin names. Each name
must be the name of a Python module under the "PLUGIN_NAMESPACE" namespace must be the name of a Python module under the "PLUGIN_NAMESPACE" namespace
package in sys.path; the module indicated should contain the package in sys.path; the module indicated should contain the
PapersPlugin subclasses desired. PapersPlugin subclasses desired.
""" """
for name in names: for name in conf['plugins']['active']:
modname = '%s.%s.%s.%s' % ('pubs', PLUGIN_NAMESPACE, name, name) modname = '%s.%s.%s.%s' % ('pubs', PLUGIN_NAMESPACE, name, name)
try: try:
namespace = importlib.import_module(modname) namespace = importlib.import_module(modname)
@ -49,7 +49,7 @@ def load_plugins(ui, names):
if isinstance(obj, type) and issubclass(obj, PapersPlugin) \ if isinstance(obj, type) and issubclass(obj, PapersPlugin) \
and obj != PapersPlugin: and obj != PapersPlugin:
_classes.append(obj) _classes.append(obj)
_instances[obj] = obj() _instances[obj] = obj(conf)
def get_plugins(): def get_plugins():

@ -2,7 +2,6 @@ import subprocess
import shlex import shlex
from ...plugins import PapersPlugin from ...plugins import PapersPlugin
from ...configs import config
from ...pubs_cmd import execute from ...pubs_cmd import execute
@ -17,8 +16,9 @@ class Alias(object):
p = parser.add_parser(self.name, help='user defined command') p = parser.add_parser(self.name, help='user defined command')
p.add_argument('arguments', nargs='*', p.add_argument('arguments', nargs='*',
help="arguments to be passed to the user defined command") help="arguments to be passed to the user defined command")
return p
def run(self, args): def command(self, conf, args):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@ -35,7 +35,7 @@ class CommandAlias(Alias):
- other arguments are passed to the command - other arguments are passed to the command
""" """
def run(self, args): def command(self, conf, args):
raw_args = ([args.prog] raw_args = ([args.prog]
+ shlex.split(self.definition + shlex.split(self.definition
+ ' ' + ' '
@ -45,7 +45,7 @@ class CommandAlias(Alias):
class ShellAlias(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 """Uses a shell function so that arguments can be used in the command
as shell arguments. as shell arguments.
""" """
@ -59,12 +59,14 @@ class AliasPlugin(PapersPlugin):
name = 'alias' name = 'alias'
def __init__(self): def __init__(self, conf):
self.aliases = [] self.aliases = []
for name, definition in config('alias').items(): if 'alias' in conf['plugins']:
for name, definition in conf['plugins']['alias'].items():
self.aliases.append(Alias.create_alias(name, definition)) self.aliases.append(Alias.create_alias(name, definition))
def get_commands(self, parser): def update_parser(self, subparsers):
for a in self.aliases: """Add subcommand to the provided subparser"""
a.parser(parser) for alias in self.aliases:
return [(a.name, a.run) for a in self.aliases] alias_parser = alias.parser(subparsers)
alias_parser.set_defaults(func=alias.command)

@ -33,10 +33,11 @@ def bib_oneliner(bibdata):
journal = ' ' + bibdata.get('booktitle', '') journal = ' ' + bibdata.get('booktitle', '')
return u'{authors} \"{title}\"{journal}{year}'.format( return u'{authors} \"{title}\"{journal}{year}'.format(
authors=color.dye_out(authors, 'bold'), authors=color.dye_out(authors, 'author'),
title=bibdata.get('title', ''), title=color.dye_out(bibdata.get('title', ''), 'title'),
journal=color.dye_out(journal, 'italic'), journal=color.dye_out(journal, 'publisher'),
year=' ({})'.format(bibdata['year']) if 'year' in bibdata else '', 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: else:
bibdesc = bib_oneliner(p.bibdata) bibdesc = bib_oneliner(p.bibdata)
tags = '' if len(p.tags) == 0 else '| {}'.format( 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( return u'[{citekey}] {descr} {tags}'.format(
citekey=color.dye_out(p.citekey, 'purple'), citekey=color.dye_out(p.citekey, 'citekey'),
descr=bibdesc, tags=tags) descr=bibdesc, tags=tags)

@ -1,17 +1,18 @@
import sys import sys
import argparse import argparse
import collections import collections
from . import uis from . import uis
from . import configs from . import config
from . import commands from . import commands
from . import update
from . import plugins from . import plugins
from .__init__ import __version__ from .__init__ import __version__
CORE_CMDS = collections.OrderedDict([ CORE_CMDS = collections.OrderedDict([
('init', commands.init_cmd), ('init', commands.init_cmd),
('conf', commands.conf_cmd),
('add', commands.add_cmd), ('add', commands.add_cmd),
('rename', commands.rename_cmd), ('rename', commands.rename_cmd),
('remove', commands.remove_cmd), ('remove', commands.remove_cmd),
@ -27,66 +28,58 @@ CORE_CMDS = collections.OrderedDict([
('websearch', commands.websearch_cmd), ('websearch', commands.websearch_cmd),
('edit', commands.edit_cmd), ('edit', commands.edit_cmd),
# ('update', commands.update_cmd),
]) ])
def _update_check(config, ui): def execute(raw_args=sys.argv):
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()
conf_parser = argparse.ArgumentParser(prog="pubs", add_help=False)
conf_parser.add_argument("-c", "--config", help="path to config file",
type=str, metavar="FILE")
#conf_parser.add_argument("-u", "--update", help="update config if needed",
# default=False, action='store_true')
top_args, remaining_args = conf_parser.parse_known_args(raw_args[1:])
def execute(raw_args=sys.argv): if top_args.config:
# loading config conf_path = top_args.config
config = configs.Config() else:
if len(raw_args) > 1 and raw_args[1] != 'init': conf_path = config.get_confpath(verify=False) # will be checked on load
# Loading config
if len(remaining_args) > 0 and remaining_args[0] != 'init':
try: try:
config.load() conf = config.load_conf(path=conf_path, check=False)
if update.update_check(conf, path=conf.filename):
# an update happened, reload conf.
conf = config.load_conf(path=conf_path, check=False)
config.check_conf(conf)
except IOError as e: except IOError as e:
print('error: {}'.format(str(e))) print('error: {}'.format(str(e)))
sys.exit() sys.exit()
config.as_global() else:
conf = config.load_default_conf()
conf.filename = conf_path
uis.init_ui(config) uis.init_ui(conf)
ui = uis.get_ui() ui = uis.get_ui()
_update_check(config, ui) parser = argparse.ArgumentParser(description="research papers repository",
prog="pubs", add_help=True)
parser = argparse.ArgumentParser(description="research papers repository") parser.add_argument('--version', action='version', version=__version__)
subparsers = parser.add_subparsers(title="valid commands", dest="command") subparsers = parser.add_subparsers(title="valid commands", dest="command")
subparsers.required = True
cmd_funcs = collections.OrderedDict() # Populate the parser with core commands
for cmd_name, cmd_mod in CORE_CMDS.items(): for cmd_name, cmd_mod in CORE_CMDS.items():
cmd_mod.parser(subparsers) cmd_parser = cmd_mod.parser(subparsers)
cmd_funcs[cmd_name] = cmd_mod.command cmd_parser.set_defaults(func=cmd_mod.command)
# Extend with plugin commands # Extend with plugin commands
plugins.load_plugins(ui, config.plugins.split()) plugins.load_plugins(conf, ui)
for p in plugins.get_plugins().values(): for p in plugins.get_plugins().values():
cmd_funcs.update(p.get_commands(subparsers)) p.update_parser(subparsers)
args = parser.parse_args(raw_args[1:])
args.prog = parser.prog # Hack: there might be a better way...
cmd = args.command
del args.command
cmd_funcs[cmd](args) # Parse and run appropriate command
args = parser.parse_args(remaining_args)
args.prog = "pubs" # FIXME?
args.func(conf, args)

@ -22,10 +22,10 @@ class InvalidReference(Exception):
class Repository(object): class Repository(object):
def __init__(self, config, create=False): def __init__(self, conf, create=False):
self.config = config self.conf = conf
self._citekeys = None self._citekeys = None
self.databroker = DataCache(self.config.pubsdir, create=create) self.databroker = DataCache(self.conf['main']['pubsdir'], create=create)
@property @property
def citekeys(self): def citekeys(self):
@ -133,7 +133,7 @@ class Repository(object):
def push_doc(self, citekey, docfile, copy=None): def push_doc(self, citekey, docfile, copy=None):
p = self.pull_paper(citekey) p = self.pull_paper(citekey)
if copy is None: if copy is None:
copy = self.config.import_copy copy = self.conf['main']['doc_add'] in ('copy', 'move')
if copy: if copy:
docfile = self.databroker.add_doc(citekey, docfile) docfile = self.databroker.add_doc(citekey, docfile)
else: else:

@ -6,6 +6,7 @@ import codecs
from .content import editor_input from .content import editor_input
from . import color from . import color
from . import config
from .p3 import _get_raw_stdout, _get_raw_stderr, input, ustr from .p3 import _get_raw_stdout, _get_raw_stderr, input, ustr
@ -30,7 +31,7 @@ def _get_encoding(conf):
def get_ui(): def get_ui():
if _ui is None: if _ui is None:
return PrintUI() # no editor support. (#FIXME?) return PrintUI(config.load_default_conf()) # no editor support. (#FIXME?)
return _ui return _ui
@ -41,16 +42,12 @@ def init_ui(conf):
class PrintUI(object): class PrintUI(object):
def __init__(self, conf=None): def __init__(self, conf):
""" """
:param conf: if None, conservative default values are used. :param conf: if None, conservative default values are used.
Useful to instanciate the UI before parsing the config file. Useful to instanciate the UI before parsing the config file.
""" """
if conf is None: color.setup(conf)
color.setup()
else:
color.setup(color=True, bold=True, italic=True)
# color.setup(color=conf.color, bold=conf.bold, italic=conf.italic)
self.encoding = _get_encoding(conf) self.encoding = _get_encoding(conf)
self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(), self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(),
errors='replace') errors='replace')
@ -63,11 +60,11 @@ class PrintUI(object):
def warning(self, message, **kwargs): def warning(self, message, **kwargs):
kwargs['file'] = self._stderr 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): def error(self, message, **kwargs):
kwargs['file'] = self._stderr 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): def exit(self, error_code=1):
sys.exit(error_code) sys.exit(error_code)
@ -79,7 +76,7 @@ class InputUI(PrintUI):
def __init__(self, conf): def __init__(self, conf):
super(InputUI, self).__init__(conf) super(InputUI, self).__init__(conf)
self.editor = conf.edit_cmd self.editor = conf['main']['edit_cmd']
def input(self): def input(self):
try: try:
@ -101,14 +98,12 @@ class InputUI(PrintUI):
:returns: int :returns: int
the index of the chosen option the index of the chosen option
""" """
char_color = 'bold'
option_chars = [s[0] for s in options] option_chars = [s[0] for s in options]
displayed_chars = [c.upper() if i == default else c displayed_chars = [c.upper() if i == default else c
for i, c in enumerate(option_chars)] 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 if len(set(option_chars)) != len(option_chars): # duplicate chars, char choices are deactivated. #FIXME: should only deactivate ambiguous chars
option_chars = [] option_chars = []
char_color = color.end
option_str = '/'.join(["{}{}".format(color.dye_out(c, 'bold'), s[1:]) option_str = '/'.join(["{}{}".format(color.dye_out(c, 'bold'), s[1:])
for c, s in zip(displayed_chars, options)]) for c, s in zip(displayed_chars, options)])

@ -0,0 +1,106 @@
import shutil
import io
from . import config
from . import uis
from . import color
from .__init__ import __version__
def update_check(conf, path=None):
"""Runs an update if necessary, and return True in that case."""
code_version = __version__.split('.')
try:
repo_version = conf['internal']['version'].split('.')
except KeyError:
repo_version = ['0', '5', '0']
if repo_version > code_version:
uis.init_ui(config.load_default_conf())
ui = uis.get_ui()
ui.warning(
'Your repository was generated with an newer version'
' of pubs (v{}) than the one you are using (v{}).'
'\n'.format(repo_version, code_version) +
'You should not use pubs until you install the '
'newest version.')
sys.exit()
elif repo_version <= code_version:
return update(conf, code_version, repo_version, path=path)
return False
def update(conf, code_version, repo_version, path=None):
"""Runs an update if necessary, and return True in that case."""
if path is None:
path = config.get_confpath()
if repo_version == ['0', '5', '0']: # we need to update
default_conf = config.load_default_conf()
for key in ['pubsdir', 'docsdir', 'edit_cmd', 'open_cmd']:
if key in conf['pubs']:
default_conf['main'][key] = conf['pubs'][key]
if conf.get('import_move'):
default_conf['main']['add_doc'] = 'move'
elif conf.get('import_copy'):
default_conf['main']['add_doc'] = 'copy'
else:
default_conf['main']['add_doc'] = 'link'
backup_path = path + '.old'
shutil.move(path, backup_path)
config.save_conf(default_conf)
uis.init_ui(default_conf)
ui = uis.get_ui()
ui.warning(
'Your configuration file has been updated. '
'Your old configuration has been moved to `{}`. '.format(color.dye_out(backup_path, 'filepath')) +
'Some, but not all, of your settings has been transferred '
'to the new file.\n'
'You can inspect and modify your configuration '
' using the `pubs config` command.'
)
return True
# continuous update while configuration is stabilizing
if repo_version == ['0', '6', '0'] and repo_version < code_version:
default_conf = config.load_default_conf()
for section_name, section in conf.items():
for key, value in section.items():
try:
default_conf[section_name][key]
default_conf[section_name][key] = value
except KeyError:
pass
# we don't update plugins
for section_name, section in conf['plugins'].items():
default_conf[section_name]['plugins'][section_name] = section
# comparing potential changes
with open(path, 'r') as f:
old_conf_text = f.read()
new_conf_text = io.BytesIO()
default_conf.write(outfile=new_conf_text)
if new_conf_text.getvalue() != old_conf_text:
backup_path = path + '.old'
shutil.move(path, backup_path)
default_conf.filename = path
config.save_conf(default_conf)
uis.init_ui(default_conf)
ui = uis.get_ui()
ui.warning('Your configuration file has been updated.\n'
'Your old configuration has been moved to `{}`.'.format(color.dye_out(backup_path, 'filepath')))
return True
return False

@ -9,13 +9,13 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True):
citekeys = repo.citekeys_from_prefix(citekey) citekeys = repo.citekeys_from_prefix(citekey)
if len(citekeys) == 0: if len(citekeys) == 0:
if ui is not None: 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: if exit_on_fail:
ui.exit() ui.exit()
elif len(citekeys) == 1: elif len(citekeys) == 1:
if citekeys[0] != citekey: if citekeys[0] != citekey:
if ui is not None: 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] citekey = citekeys[0]
elif citekey not in citekeys: elif citekey not in citekeys:
if ui is not None: if ui is not None:

@ -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. **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/'). Create your library (by default, goes to '~/.pubs/').
pubs init pubs init
@ -36,8 +36,8 @@ or an ISBN (dashes are ignored):
pubs add -I 978-0822324669 -d article.pdf 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: If you use latex, you can automatize references, by creating a bash script with:
#!/bin/bash #!/bin/bash
@ -52,28 +52,28 @@ This ensure that your reference file is always up-to-date; you can cite a paper
and then add `\cite{Loeb_2012}` in your manuscript. After running the bash script, the citation will correctly appear in your compiled pdf. and then add `\cite{Loeb_2012}` in your manuscript. After running the bash script, the citation will correctly appear in your compiled pdf.
Customization ## 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. Pubs is designed to interact well with your command line tool chain. You can add custom commands to pubs by defining aliases in your config file.
[alias] [[alias]]
print = open -w lp evince = open --with evince
count = !pubs list -k | wc -l count = !pubs list -k | wc -l
For more advanced functionalities, pubs also support plugins. Actually *alias* is itself a plugin! The first command defines a new subcommand: `pubs open -w lp` will be executed when `pubs print` is typed.
The second starts with a bang: `!`, and is treated as a shell command.
## Requirements
Requirements
------------
- python >= 2.7 or >= 3.3 - python >= 2.7 or >= 3.3
- [dateutil](http://labix.org/python-dateutil) - [dateutil](http://labix.org/python-dateutil)
- [pyYaml](http://pyyaml.org) (will be deprecated soon) - [pyYaml](http://pyyaml.org) (will be deprecated soon)
- [bibtexparser](https://github.com/sciunto/python-bibtexparser) >= 0.6.1 - [bibtexparser](https://github.com/sciunto/python-bibtexparser) >= 0.6.1
Authors ## Authors
-------
- [Fabien Benureau](http://fabien.benureau.com) - [Fabien Benureau](http://fabien.benureau.com)
- Olivier Mangin - Olivier Mangin
- Jonathan Grizou - Jonathan Grizou
- Arnold Sykosch

@ -2,7 +2,7 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
VERSION = '0.5.0' VERSION = '0.6.0'
setup( setup(
name = 'pubs', name = 'pubs',
@ -13,10 +13,13 @@ setup(
url = 'https://github.com/pubs/pubs', url = 'https://github.com/pubs/pubs',
description = 'command-line scientific bibliography manager', description = 'command-line scientific bibliography manager',
packages = ['pubs', 'pubs.commands', 'pubs.templates', 'pubs.plugs'], packages = ['pubs', 'pubs.config',
'pubs.commands',
'pubs.templates',
'pubs.plugs', 'pubs.plugs.alias'],
scripts = ['pubs/pubs'], scripts = ['pubs/pubs'],
install_requires = ['pyyaml', 'bibtexparser', 'python-dateutil', 'requests', install_requires = ['pyyaml', 'bibtexparser', 'python-dateutil', 'requests', 'configobj',
'beautifulsoup4'], # to be made optional? 'beautifulsoup4'], # to be made optional?
classifiers=[ classifiers=[

@ -103,6 +103,8 @@ class FakeIO(object):
fakefs_stringio = self.fake_open.Call(*args, **kwargs) fakefs_stringio = self.fake_open.Call(*args, **kwargs)
return UnicodeStringIOWrapper(fakefs_stringio) return UnicodeStringIOWrapper(fakefs_stringio)
BytesIO = real_io.BytesIO
StringIO = real_io.StringIO
def create_fake_fs(module_list): def create_fake_fs(module_list):

@ -5,7 +5,7 @@ from pubs import color
def perf_color(): def perf_color():
s = str(list(range(1000))) s = str(list(range(1000)))
for _ in range(5000000): for _ in range(5000000):
color.dye(s, color.red) color.dye_out(s, 'red')
if __name__ == '__main__': if __name__ == '__main__':

@ -2,71 +2,70 @@
import unittest import unittest
import dotdot import dotdot
from pubs import configs from pubs.config import conf
from pubs.configs import config
from pubs.p3 import configparser from pubs.p3 import configparser
class TestConfig(unittest.TestCase): # class TestConfig(unittest.TestCase):
#
def test_create_config(self): # def test_create_config(self):
a = configs.Config() # a = configs.Config()
a.as_global() # a.as_global()
self.assertEqual(a, config()) # self.assertEqual(a, config())
#
def test_config_content(self): # def test_config_content(self):
a = configs.Config() # a = configs.Config()
a.as_global() # a.as_global()
#
self.assertEqual(config().pubsdir, configs.DFT_CONFIG['pubsdir']) # self.assertEqual(config().pubsdir, configs.DFT_CONFIG['pubsdir'])
self.assertEqual(config().color, configs.str2bool(configs.DFT_CONFIG['color'])) # self.assertEqual(config().color, configs.str2bool(configs.DFT_CONFIG['color']))
#
def test_set(self): # def test_set(self):
a = configs.Config() # a = configs.Config()
a.as_global() # a.as_global()
config().color = 'no' # config().color = 'no'
self.assertEqual(config().color, False) # self.assertEqual(config().color, False)
self.assertEqual(config('pubs').color, False) # self.assertEqual(config('pubs').color, False)
# booleans type for new variables are memorized, but not saved. # # booleans type for new variables are memorized, but not saved.
config().bla = True # config().bla = True
self.assertEqual(config().bla, True) # self.assertEqual(config().bla, True)
self.assertEqual(config('pubs').bla, True) # self.assertEqual(config('pubs').bla, True)
#
with self.assertRaises(configparser.NoOptionError): # with self.assertRaises(configparser.NoOptionError):
config()._cfg.get(configs.MAIN_SECTION, '_section') # config()._cfg.get(configs.MAIN_SECTION, '_section')
#
def test_reload(self): # def test_reload(self):
#
default_color = configs.DFT_CONFIG['color'] # default_color = configs.DFT_CONFIG['color']
#
a = configs.Config() # a = configs.Config()
a.as_global() # a.as_global()
a.color = False # a.color = False
a.bla = 'foo' # a.bla = 'foo'
config.color = not configs.str2bool(default_color) # config.color = not configs.str2bool(default_color)
self.assertEqual(config().color, not configs.str2bool(default_color)) # self.assertEqual(config().color, not configs.str2bool(default_color))
#
b = configs.Config() # b = configs.Config()
b.as_global() # b.as_global()
self.assertEqual(b, config()) # self.assertEqual(b, config())
self.assertEqual(config().color, configs.str2bool(default_color)) # self.assertEqual(config().color, configs.str2bool(default_color))
#
def test_exception(self): # def test_exception(self):
#
a = configs.Config() # a = configs.Config()
a.as_global() # a.as_global()
#
with self.assertRaises(configparser.NoOptionError): # with self.assertRaises(configparser.NoOptionError):
config().color2 # config().color2
self.assertEqual(config().get('color2', default = 'blue'), 'blue') # self.assertEqual(config().get('color2', default = 'blue'), 'blue')
#
with self.assertRaises(configparser.NoSectionError): # with self.assertRaises(configparser.NoSectionError):
config(section = 'bla3').color # config(section = 'bla3').color
self.assertEqual(config(section = 'bla3').get('color', default = 'green'), 'green') # self.assertEqual(config(section = 'bla3').get('color', default = 'green'), 'green')
self.assertEqual(config(section = 'bla3').get('color', default = config().color), True) # self.assertEqual(config(section = 'bla3').get('color', default = config().color), True)
#
def test_keywords(self): # def test_keywords(self):
a = configs.Config(pubs_dir = '/blabla') # a = configs.Config(pubs_dir = '/blabla')
self.assertEqual(a.pubs_dir, '/blabla') # self.assertEqual(a.pubs_dir, '/blabla')
if __name__ == '__main__': if __name__ == '__main__':

@ -5,7 +5,8 @@ import os
import dotdot import dotdot
import fake_env 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 import str_fixtures
from pubs import endecoder from pubs import endecoder
@ -20,7 +21,7 @@ class TestDataBroker(unittest.TestCase):
page99_bibentry = ende.decode_bibdata(str_fixtures.bibtex_raw0) page99_bibentry = ende.decode_bibdata(str_fixtures.bibtex_raw0)
for db_class in [databroker.DataBroker, datacache.DataCache]: 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) db = db_class('tmp', create=True)

@ -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)

@ -5,7 +5,7 @@ import os
import dotdot import dotdot
import fake_env import fake_env
from pubs import endecoder, pretty, color from pubs import endecoder, pretty, color, config
from str_fixtures import bibtex_raw0 from str_fixtures import bibtex_raw0
@ -13,7 +13,8 @@ from str_fixtures import bibtex_raw0
class TestPretty(unittest.TestCase): class TestPretty(unittest.TestCase):
def setUp(self): def setUp(self):
color.setup() conf = config.load_default_conf()
color.setup(conf)
def test_oneliner(self): def test_oneliner(self):
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()

@ -7,14 +7,15 @@ import fixtures
from pubs.repo import Repository, _base27, CiteKeyCollision, InvalidReference from pubs.repo import Repository, _base27, CiteKeyCollision, InvalidReference
from pubs.paper import Paper from pubs.paper import Paper
from pubs import configs from pubs import config
class TestRepo(fake_env.TestFakeFs): class TestRepo(fake_env.TestFakeFs):
def setUp(self): def setUp(self):
super(TestRepo, self).setUp() 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)) self.repo.push_paper(Paper.from_bibentry(fixtures.turing_bibentry))

@ -7,8 +7,9 @@ import os
import dotdot import dotdot
import fake_env import fake_env
from pubs import pubs_cmd from pubs import pubs_cmd, update, color, content, filebroker, uis, p3, endecoder
from pubs import color, content, filebroker, uis, p3, endecoder, configs from pubs.config import conf
import configobj
import str_fixtures import str_fixtures
import fixtures import fixtures
@ -17,7 +18,7 @@ from pubs.commands import init_cmd, import_cmd
# makes the tests very noisy # makes the tests very noisy
messagePUT=False PRINT_OUTPUT=False
CAPTURE_OUTPUT=True CAPTURE_OUTPUT=True
@ -56,8 +57,9 @@ class CommandTestCase(unittest.TestCase):
maxDiff = 1000000 maxDiff = 1000000
def setUp(self): def setUp(self):
self.fs = fake_env.create_fake_fs([content, filebroker, configs, init_cmd, import_cmd]) self.fs = fake_env.create_fake_fs([content, filebroker, conf, init_cmd, import_cmd, configobj, update])
self.default_pubs_dir = self.fs['os'].path.expanduser('~/.pubs') self.default_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): def execute_cmds(self, cmds, capture_output=CAPTURE_OUTPUT):
""" Execute a list of commands, and capture their output """ Execute a list of commands, and capture their output
@ -102,12 +104,12 @@ class CommandTestCase(unittest.TestCase):
except fake_env.FakeInput.UnexpectedInput: except fake_env.FakeInput.UnexpectedInput:
self.fail('Unexpected input asked by command: {}.'.format( self.fail('Unexpected input asked by command: {}.'.format(
actual_cmd)) actual_cmd))
if messagePUT: if PRINT_OUTPUT:
print(outs) print(outs)
return outs return outs
def tearDown(self): 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): class DataCommandTestCase(CommandTestCase):
@ -323,7 +325,8 @@ class TestUsecase(DataCommandTestCase):
def test_first(self): def test_first(self):
correct = ['Initializing pubs in /paper_first\n', 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', '[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n',
'\n', '\n',
'', '',
@ -507,8 +510,21 @@ class TestUsecase(DataCommandTestCase):
'pubs attach --move Page99 data/pagerank.pdf' 'pubs attach --move Page99 data/pagerank.pdf'
] ]
self.execute_cmds(cmds) 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')) 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__': if __name__ == '__main__':
unittest.main() unittest.main()

Loading…
Cancel
Save