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 from .. import pretty
def parser(subparsers): def parser(subparsers, conf):
parser = subparsers.add_parser('add', help='add a paper to the repository') parser = subparsers.add_parser('add', help='add a paper to the repository')
parser.add_argument('bibfile', nargs='?', default=None, parser.add_argument('bibfile', nargs='?', default=None,
help='bibtex file') help='bibtex file')

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

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

@ -4,15 +4,19 @@ from .. import repo
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
from ..completion import CiteKeyCompletion
def parser(subparsers): def parser(subparsers, conf):
parser = subparsers.add_parser('edit', parser = subparsers.add_parser(
'edit',
help='open the paper bibliographic file in an editor') 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') help='edit metadata')
parser.add_argument('citekey', parser.add_argument(
help='citekey of the paper') 'citekey',
help='citekey of the paper').completer = CiteKeyCompletion(conf)
return parser return parser

@ -4,12 +4,15 @@ from .. import repo
from ..uis import get_ui from ..uis import get_ui
from .. import endecoder from .. import endecoder
from ..utils import resolve_citekey_list 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 = subparsers.add_parser('export', help='export bibliography')
# parser.add_argument('-f', '--bib-format', default='bibtex', # parser.add_argument('-f', '--bib-format', default='bibtex',
# help='export format') # 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 return parser

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

@ -9,7 +9,8 @@ from ..repo import Repository
from ..content import system_path, check_directory from ..content import system_path, check_directory
from .. import config from .. import config
def parser(subparsers):
def parser(subparsers, conf):
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,

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

@ -2,19 +2,19 @@ from .. import repo
from .. import content from .. import content
from ..uis import get_ui from ..uis import get_ui
from ..utils import resolve_citekey from ..utils import resolve_citekey
from ..completion import CiteKeyCompletion
def parser(subparsers): def parser(subparsers, conf):
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')
parser.add_argument('citekey', parser.add_argument('citekey',
help='citekey of the paper') help='citekey of the paper'
).completer = CiteKeyCompletion(conf)
return parser return parser
def command(conf, args): def command(conf, args):
"""
"""
ui = get_ui() ui = get_ui()
rp = repo.Repository(conf) rp = repo.Repository(conf)

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

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

@ -24,15 +24,17 @@ from ..uis import get_ui
from .. import pretty from .. import pretty
from .. import color from .. import color
from ..utils import resolve_citekey 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 = subparsers.add_parser('tag', help="add, remove and show tags")
parser.add_argument('citekeyOrTag', nargs='?', default=None, 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, parser.add_argument('tags', nargs='?', default=None,
help='If the previous argument was a citekey, then ' 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, # TODO find a way to display clear help for multiple command semantics,
# indistinguisable for argparse. (fabien, 201306) # indistinguisable for argparse. (fabien, 201306)
return parser return parser
@ -70,6 +72,7 @@ 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(conf, args):
"""Add, remove and show tags""" """Add, remove and show tags"""

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

@ -1,7 +1,5 @@
import os import os
import platform import platform
import shutil
import configobj import configobj
import validate import validate
@ -11,6 +9,16 @@ from .spec import configspec
DFT_CONFIG_PATH = os.path.expanduser('~/.pubsrc') 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): def post_process_conf(conf):
"""Do some post processing on the configuration""" """Do some post processing on the configuration"""
if conf['main']['docsdir'] == 'docsdir://': 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 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""" """Load the configuration"""
if path is None: if path is None:
path = get_confpath(verify=True) path = get_confpath(verify=True)
if not os.path.exists(path):
raise ConfigurationNotFound(path)
with open(path, 'rb') as f: with open(path, 'rb') as f:
conf = configobj.ConfigObj(f.readlines(), configspec=configspec) conf = configobj.ConfigObj(f.readlines(), configspec=configspec)
if check:
check_conf(conf)
conf.filename = path conf.filename = path
conf = post_process_conf(conf) conf = post_process_conf(conf)
return conf return conf

@ -14,7 +14,7 @@ class PapersPlugin(object):
name = None name = None
def get_commands(self, subparsers): def get_commands(self, subparsers, conf):
"""Populates the parser with plugins specific command. """Populates the parser with plugins specific command.
Returns iterable of pairs (command name, command function to call). 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(): 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 update_parser(self, subparsers): def update_parser(self, subparsers, conf):
"""Add subcommand to the provided subparser""" """Add subcommand to the provided subparser"""
for alias in self.aliases: for alias in self.aliases:
alias_parser = alias.parser(subparsers) alias_parser = alias.parser(subparsers)

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

@ -7,6 +7,7 @@ from . import commands
from . import update from . import update
from . import plugins from . import plugins
from .__init__ import __version__ from .__init__ import __version__
from .completion import autocomplete
CORE_CMDS = collections.OrderedDict([ 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 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) conf = config.load_conf(path=conf_path)
if update.update_check(conf, path=conf.filename): if update.update_check(conf, path=conf.filename):
# an update happened, reload conf. # 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) config.check_conf(conf)
else: except config.ConfigurationNotFound:
if len(remaining_args) == 0 or remaining_args[0] == 'init':
conf = config.load_default_conf() conf = config.load_default_conf()
conf.filename = conf_path conf.filename = conf_path
else:
raise
uis.init_ui(conf, force_colors=top_args.force_colors) uis.init_ui(conf, force_colors=top_args.force_colors)
ui = uis.get_ui() ui = uis.get_ui()
@ -70,14 +74,16 @@ def execute(raw_args=sys.argv):
# Populate the parser with core commands # 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_parser = cmd_mod.parser(subparsers) cmd_parser = cmd_mod.parser(subparsers, conf)
cmd_parser.set_defaults(func=cmd_mod.command) cmd_parser.set_defaults(func=cmd_mod.command)
# Extend with plugin commands # Extend with plugin commands
plugins.load_plugins(conf, ui) plugins.load_plugins(conf, ui)
for p in plugins.get_plugins().values(): 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 # Parse and run appropriate command
args = parser.parse_args(remaining_args) args = parser.parse_args(remaining_args)
args.prog = "pubs" # FIXME? args.prog = "pubs" # FIXME?

@ -1,6 +1,7 @@
import shutil import shutil
import io import io
import sys
from . import config from . import config
from . import uis from . import uis
from . import color from . import color
@ -33,6 +34,7 @@ def update_check(conf, path=None):
return False return False
def update(conf, code_version, repo_version, path=None): def update(conf, code_version, repo_version, path=None):
"""Runs an update if necessary, and return True in that case.""" """Runs an update if necessary, and return True in that case."""
if path is None: if path is None:

@ -15,18 +15,20 @@ Pubs is built with the following principles in mind:
## Installation ## 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 pip install --upgrade git+https://github.com/pubs/pubs
cd pubs
sudo python setup.py install # remove sudo and add --user for local installation instead 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. Alternatively Arch Linux users can also use the [pubs-git](https://aur.archlinux.org/packages/pubs-git/) AUR package.
## Getting started ## Getting started
Create your library (by default, goes to '~/.pubs/'). Create your library (by default, goes to `~/.pubs/`).
pubs init 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. 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 ? ## 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 ## Requirements
- python >= 2.7 or >= 3.3 - python >= 2.7 or >= 3.3
- [argcomplete](https://argcomplete.readthedocs.io) (optional, for autocompletion)
## Authors ## Authors

@ -33,7 +33,7 @@ class FakeSystemExit(Exception):
SystemExit exceptions are replaced by FakeSystemExit in the execute_cmds() SystemExit exceptions are replaced by FakeSystemExit in the execute_cmds()
function, so they can be catched by ExpectedFailure tests in Python 2.x. 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 pass
@ -151,6 +151,13 @@ class DataCommandTestCase(CommandTestCase):
# Actual tests # Actual tests
class TestAlone(CommandTestCase):
def test_alone_misses_command(self):
with self.assertRaises(FakeSystemExit):
self.execute_cmds(['pubs'])
class TestInit(CommandTestCase): class TestInit(CommandTestCase):
def test_init(self): def test_init(self):

Loading…
Cancel
Save