Merge pull request #61 from pubs/feat/autocomplete

Feat/autocomplete
main
Fabien Benureau 8 years ago committed by GitHub
commit 2465f821ba

@ -9,7 +9,7 @@ from .. import color
from .. import pretty
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('add', help='add a paper to the repository')
parser.add_argument('bibfile', nargs='?', default=None,
help='bibtex file')

@ -3,7 +3,7 @@ from .. import config
from .. import content
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('conf',
help='open the configuration in an editor')
return parser
@ -17,7 +17,7 @@ def command(conf, args):
# get modif from user
ui.edit_file(config.get_confpath())
new_conf = config.load_conf(check=False)
new_conf = config.load_conf()
try:
config.check_conf(new_conf)
ui.message('The configuration file was updated.')

@ -6,6 +6,8 @@ from .. import color
from ..uis import get_ui
from .. import content
from ..utils import resolve_citekey, resolve_citekey_list
from ..completion import CiteKeyCompletion
# doc --+- add $file $key [[-L|--link] | [-M|--move]] [-f|--force]
# +- remove $key [$key [...]] [-f|--force]
@ -13,35 +15,46 @@ from ..utils import resolve_citekey, resolve_citekey_list
# +- open $key [-w|--with $cmd]
# supplements attach, open
def parser(subparsers):
doc_parser = subparsers.add_parser('doc', help='manage the document relating to a publication')
doc_subparsers = doc_parser.add_subparsers(title='document actions', help='actions to interact with the documents',
dest='action')
def parser(subparsers, conf):
doc_parser = subparsers.add_parser(
'doc',
help='manage the document relating to a publication')
doc_subparsers = doc_parser.add_subparsers(
title='document actions', dest='action',
help='actions to interact with the documents')
doc_subparsers.required = True
add_parser = doc_subparsers.add_parser('add', help='add a document to a publication')
add_parser.add_argument('-f', '--force', action='store_true', dest='force', default=False,
help='force overwriting an already assigned document')
add_parser.add_argument('document', nargs=1, help='document file to assign')
add_parser.add_argument('citekey', nargs=1, help='citekey of the publication')
add_parser.add_argument('citekey', nargs=1, help='citekey of the publication'
).completer = CiteKeyCompletion(conf)
add_exclusives = add_parser.add_mutually_exclusive_group()
add_exclusives.add_argument('-L', '--link', action='store_false', dest='link', default=False,
add_exclusives.add_argument(
'-L', '--link', action='store_false', dest='link', default=False,
help='do not copy document files, just create a link')
add_exclusives.add_argument('-M', '--move', action='store_true', dest='move', default=False,
add_exclusives.add_argument(
'-M', '--move', action='store_true', dest='move', default=False,
help='move document instead of of copying (ignored if --link)')
remove_parser = doc_subparsers.add_parser('remove', help='remove assigned documents from publications')
remove_parser.add_argument('citekeys', nargs='+', help='citekeys of the publications')
remove_parser.add_argument('-f', '--force', action='store_true', dest='force', default=False,
remove_parser.add_argument('citekeys', nargs='+', help='citekeys of the publications'
).completer = CiteKeyCompletion(conf)
remove_parser.add_argument('-f', '--force', action='store_true', dest='force',
default=False,
help='force removing assigned documents')
# favor key+ path over: key
export_parser = doc_subparsers.add_parser('export', help='export assigned documents to given path')
export_parser.add_argument('citekeys', nargs='+', help='citekeys of the documents to export')
export_parser.add_argument('citekeys', nargs='+',
help='citekeys of the documents to export'
).completer = CiteKeyCompletion(conf)
export_parser.add_argument('path', nargs=1, help='directory to export the files to')
open_parser = doc_subparsers.add_parser('open', help='open an assigned document')
open_parser.add_argument('citekey', nargs=1, help='citekey of the document to open')
open_parser.add_argument('citekey', nargs=1, help='citekey of the document to open'
).completer = CiteKeyCompletion(conf)
open_parser.add_argument('-w', '--with', dest='cmd', help='command to open the file with')
return doc_parser

@ -4,15 +4,19 @@ from .. import repo
from ..uis import get_ui
from ..endecoder import EnDecoder
from ..utils import resolve_citekey
from ..completion import CiteKeyCompletion
def parser(subparsers):
parser = subparsers.add_parser('edit',
def parser(subparsers, conf):
parser = subparsers.add_parser(
'edit',
help='open the paper bibliographic file in an editor')
parser.add_argument('-m', '--meta', action='store_true', default=False,
parser.add_argument(
'-m', '--meta', action='store_true', default=False,
help='edit metadata')
parser.add_argument('citekey',
help='citekey of the paper')
parser.add_argument(
'citekey',
help='citekey of the paper').completer = CiteKeyCompletion(conf)
return parser

@ -4,12 +4,15 @@ from .. import repo
from ..uis import get_ui
from .. import endecoder
from ..utils import resolve_citekey_list
from ..completion import CiteKeyCompletion
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('export', help='export bibliography')
# parser.add_argument('-f', '--bib-format', default='bibtex',
# help='export format')
parser.add_argument('citekeys', nargs='*', help='one or several citekeys')
parser.add_argument('citekeys', nargs='*', help='one or several citekeys'
).completer = CiteKeyCompletion(conf)
return parser

@ -11,7 +11,7 @@ from ..uis import get_ui
from ..content import system_path, read_text_file
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('import',
help='import paper(s) to the repository')
parser.add_argument('bibpath',

@ -9,7 +9,8 @@ from ..repo import Repository
from ..content import system_path, check_directory
from .. import config
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('init',
help="initialize the pubs directory")
parser.add_argument('-p', '--pubsdir', default=None,

@ -10,7 +10,7 @@ class InvalidQuery(ValueError):
pass
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('list', help="list papers")
parser.add_argument('-k', '--citekeys-only', action='store_true',
default=False, dest='citekeys',

@ -2,19 +2,19 @@ from .. import repo
from .. import content
from ..uis import get_ui
from ..utils import resolve_citekey
from ..completion import CiteKeyCompletion
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('note',
help='edit the note attached to a paper')
parser.add_argument('citekey',
help='citekey of the paper')
help='citekey of the paper'
).completer = CiteKeyCompletion(conf)
return parser
def command(conf, args):
"""
"""
ui = get_ui()
rp = repo.Repository(conf)

@ -3,13 +3,16 @@ from .. import color
from ..uis import get_ui
from ..utils import resolve_citekey_list
from ..p3 import ustr
from ..completion import CiteKeyCompletion
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('remove', help='removes a publication')
parser.add_argument('-f', '--force', action='store_true', default=None,
help="does not prompt for confirmation.")
parser.add_argument('citekeys', nargs='+',
help="one or several citekeys")
help="one or several citekeys"
).completer = CiteKeyCompletion(conf)
return parser

@ -2,13 +2,15 @@ from ..uis import get_ui
from .. import color
from .. import repo
from ..utils import resolve_citekey
from ..completion import CiteKeyCompletion
def parser(subparsers):
parser = subparsers.add_parser('rename', help='rename the citekey of a repository')
parser.add_argument('citekey',
help='current citekey')
parser.add_argument('new_citekey',
help='new citekey')
def parser(subparsers, conf):
parser = subparsers.add_parser('rename',
help='rename the citekey of a repository')
parser.add_argument('citekey', help='current citekey'
).completer = CiteKeyCompletion(conf)
parser.add_argument('new_citekey', help='new citekey')
return parser

@ -24,15 +24,17 @@ from ..uis import get_ui
from .. import pretty
from .. import color
from ..utils import resolve_citekey
from ..completion import CiteKeyOrTagCompletion, TagModifierCompletion
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('tag', help="add, remove and show tags")
parser.add_argument('citekeyOrTag', nargs='?', default=None,
help='citekey or tag.')
help='citekey or tag.').completer = CiteKeyOrTagCompletion(conf)
parser.add_argument('tags', nargs='?', default=None,
help='If the previous argument was a citekey, then '
'a list of tags separated by a +.')
'a list of tags separated by + and -.'
).completer = TagModifierCompletion(conf)
# TODO find a way to display clear help for multiple command semantics,
# indistinguisable for argparse. (fabien, 201306)
return parser
@ -70,6 +72,7 @@ def _tag_groups(tags):
minus_tags.append(tag[1:])
return set(plus_tags), set(minus_tags)
def command(conf, args):
"""Add, remove and show tags"""

@ -3,7 +3,8 @@ import urllib
from ..uis import get_ui
def parser(subparsers):
def parser(subparsers, conf):
parser = subparsers.add_parser('websearch',
help="launch a search on Google Scholar")
parser.add_argument("search_string", nargs = '*',

@ -0,0 +1,59 @@
import re
try:
import argcomplete
except ImportError:
class FakeModule:
@staticmethod
def _fun(*args, **kwargs):
pass
def __getattr__(self, _):
return self._fun
argcomplete = FakeModule()
from . import repo
def autocomplete(parser):
argcomplete.autocomplete(parser)
class BaseCompleter(object):
def __init__(self, conf):
self.conf = conf
def __call__(self, **kwargs):
try:
return self._complete(**kwargs)
except Exception as e:
argcomplete.warn(e)
class CiteKeyCompletion(BaseCompleter):
def _complete(self, **kwargs):
rp = repo.Repository(self.conf)
return rp.citekeys
class CiteKeyOrTagCompletion(BaseCompleter):
def _complete(self, **kwargs):
rp = repo.Repository(self.conf)
return rp.citekeys.union(rp.get_tags())
class TagModifierCompletion(BaseCompleter):
regxp = r"[^:+-]*$" # prefix of tag after last separator
def _complete(self, prefix, **kwargs):
tags = repo.Repository(self.conf).get_tags()
start, _ = re.search(self.regxp, prefix).span()
partial_expr = prefix[:start]
t_prefix = prefix[start:]
return [partial_expr + t for t in tags if t.startswith(t_prefix)]

@ -1,2 +1,3 @@
from .conf import get_confpath, load_default_conf, load_conf, save_conf, check_conf
from .conf import (get_confpath, load_default_conf, load_conf, save_conf,
check_conf, ConfigurationNotFound)
from .conf import default_open_cmd, post_process_conf

@ -1,7 +1,5 @@
import os
import platform
import shutil
import configobj
import validate
@ -11,6 +9,16 @@ from .spec import configspec
DFT_CONFIG_PATH = os.path.expanduser('~/.pubsrc')
class ConfigurationNotFound(IOError):
def __init__(self, path):
super(ConfigurationNotFound, self).__init__(
"No configuration found at path {}. Maybe you need to initialize "
"your repository with `pubs init` or specify a --config argument."
"".format(path))
def post_process_conf(conf):
"""Do some post processing on the configuration"""
if conf['main']['docsdir'] == 'docsdir://':
@ -50,14 +58,14 @@ def check_conf(conf):
assert results == True, '{}'.format(results) # TODO: precise error dialog when parsing error
def load_conf(check=True, path=None):
def load_conf(path=None):
"""Load the configuration"""
if path is None:
path = get_confpath(verify=True)
if not os.path.exists(path):
raise ConfigurationNotFound(path)
with open(path, 'rb') as f:
conf = configobj.ConfigObj(f.readlines(), configspec=configspec)
if check:
check_conf(conf)
conf.filename = path
conf = post_process_conf(conf)
return conf

@ -14,7 +14,7 @@ class PapersPlugin(object):
name = None
def get_commands(self, subparsers):
def get_commands(self, subparsers, conf):
"""Populates the parser with plugins specific command.
Returns iterable of pairs (command name, command function to call).
"""

@ -65,7 +65,7 @@ class AliasPlugin(PapersPlugin):
for name, definition in conf['plugins']['alias'].items():
self.aliases.append(Alias.create_alias(name, definition))
def update_parser(self, subparsers):
def update_parser(self, subparsers, conf):
"""Add subcommand to the provided subparser"""
for alias in self.aliases:
alias_parser = alias.parser(subparsers)

@ -1,5 +1,6 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# PYTHON_ARGCOMPLETE_OK
from pubs import pubs_cmd

@ -7,6 +7,7 @@ from . import commands
from . import update
from . import plugins
from .__init__ import __version__
from .completion import autocomplete
CORE_CMDS = collections.OrderedDict([
@ -49,15 +50,18 @@ def execute(raw_args=sys.argv):
conf_path = config.get_confpath(verify=False) # will be checked on load
# Loading config
if len(remaining_args) > 0 and remaining_args[0] != 'init':
conf = config.load_conf(path=conf_path, check=False)
try:
conf = config.load_conf(path=conf_path)
if update.update_check(conf, path=conf.filename):
# an update happened, reload conf.
conf = config.load_conf(path=conf_path, check=False)
conf = config.load_conf(path=conf_path)
config.check_conf(conf)
else:
except config.ConfigurationNotFound:
if len(remaining_args) == 0 or remaining_args[0] == 'init':
conf = config.load_default_conf()
conf.filename = conf_path
else:
raise
uis.init_ui(conf, force_colors=top_args.force_colors)
ui = uis.get_ui()
@ -70,14 +74,16 @@ def execute(raw_args=sys.argv):
# Populate the parser with core commands
for cmd_name, cmd_mod in CORE_CMDS.items():
cmd_parser = cmd_mod.parser(subparsers)
cmd_parser = cmd_mod.parser(subparsers, conf)
cmd_parser.set_defaults(func=cmd_mod.command)
# Extend with plugin commands
plugins.load_plugins(conf, ui)
for p in plugins.get_plugins().values():
p.update_parser(subparsers)
p.update_parser(subparsers, conf)
# Eventually autocomplete
autocomplete(parser)
# Parse and run appropriate command
args = parser.parse_args(remaining_args)
args.prog = "pubs" # FIXME?

@ -1,6 +1,7 @@
import shutil
import io
import sys
from . import config
from . import uis
from . import color
@ -33,6 +34,7 @@ def update_check(conf, path=None):
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:

@ -15,18 +15,20 @@ Pubs is built with the following principles in mind:
## Installation
Until pubs is uploaded to Pypi, the standard way to install it is to clone the repository and call `setup.py`.
Currently, the Pypi version is outdated. You can install the development version of `pubs`, which should be stable, with:
git clone https://github.com/pubs/pubs.git
cd pubs
sudo python setup.py install # remove sudo and add --user for local installation instead
pip install --upgrade git+https://github.com/pubs/pubs
If `pubs` is already installed, you can upgrade with:
pip install --upgrade git+https://github.com/pubs/pubs
Alternatively Arch Linux users can also use the [pubs-git](https://aur.archlinux.org/packages/pubs-git/) AUR package.
## Getting started
Create your library (by default, goes to '~/.pubs/').
Create your library (by default, goes to `~/.pubs/`).
pubs init
@ -88,15 +90,29 @@ The first command defines a new subcommand: `pubs open -w evince` will be execut
The second starts with a bang: `!`, and is treated as a shell command.
## Autocompletion
For autocompletion to work, you need the [argcomplete](https://argcomplete.readthedocs.io) Python package, and Bash 4.2 or newer. For activating *bash* or *tsch* completion, consult the [argcomplete documentation](https://argcomplete.readthedocs.io/en/latest/#global-completion).
For *zsh* completion, the global activation is not supported but bash completion compatibility can be used for pubs. For that, add the following to your `.zshrc`:
# Enable and load bashcompinit
autoload -Uz compinit bashcompinit
compinit
bashcompinit
# Argcomplete explicit registration for pubs
eval "$(register-python-argcomplete pubs)"
## Need more help ?
You can access the self-documented configuration by using `pubs conf`, and all the commands's help is available with the `--help` option. Did not find an answer to your question? Drop us an issue. We may not answer right away (science comes first!) but we'll eventually look into it.
You can access the self-documented configuration by using `pubs conf`, and all the commands' help is available with the `--help` option. Did not find an answer to your question? Drop us an issue. We may not answer right away (science comes first!) but we'll eventually look into it.
## Requirements
- python >= 2.7 or >= 3.3
- [argcomplete](https://argcomplete.readthedocs.io) (optional, for autocompletion)
## Authors

@ -33,7 +33,7 @@ class FakeSystemExit(Exception):
SystemExit exceptions are replaced by FakeSystemExit in the execute_cmds()
function, so they can be catched by ExpectedFailure tests in Python 2.x.
If a code is accepted to raise SystemExit, catch FakeSystemExit instead.
If a code is expected to raise SystemExit, catch FakeSystemExit instead.
"""
pass
@ -151,6 +151,13 @@ class DataCommandTestCase(CommandTestCase):
# Actual tests
class TestAlone(CommandTestCase):
def test_alone_misses_command(self):
with self.assertRaises(FakeSystemExit):
self.execute_cmds(['pubs'])
class TestInit(CommandTestCase):
def test_init(self):

Loading…
Cancel
Save