Merge pull request #197 from pubs/pr-191

Pr 191 - Git plugin by Amlesh Sivanantham.
main
Fabien C. Y. Benureau 6 years ago committed by GitHub
commit eedd342a2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,8 +7,10 @@
### Implemented enhancements ### Implemented enhancements
- New git plugin to commit changes to the repository ([#191](https://github.com/pubs/pubs/pull/191) by [Amlesh Sivanantham](http://github.com/zamlz))
- The import command now warn, rather than fail on existing citekeys. ([#198](https://github.com/pubs/pubs/pull/198) by [Kyle Sunden](https://github.com/ksunden))
- Add `citekey` filter to `query` ([#193](https://github.com/pubs/pubs/pull/193) by [Shane Stone](https://github.com/shanewstone)) - Add `citekey` filter to `query` ([#193](https://github.com/pubs/pubs/pull/193) by [Shane Stone](https://github.com/shanewstone))
- The `--config` and `--force-colors` command line options now appear when invoking `pubs --help`
### Fixed bugs ### Fixed bugs

@ -7,6 +7,7 @@ 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 from ..completion import CiteKeyCompletion
from ..events import ModifyEvent
def parser(subparsers, conf): def parser(subparsers, conf):
@ -88,4 +89,8 @@ def command(conf, args):
# else edit again # else edit again
# Also handle malformed bibtex and metadata # Also handle malformed bibtex and metadata
if meta:
ModifyEvent(citekey, "metadata").send()
else:
ModifyEvent(citekey, "bibtex").send()
rp.close() rp.close()

@ -2,6 +2,7 @@ from .. import repo
from ..uis import get_ui from ..uis import get_ui
from ..utils import resolve_citekey from ..utils import resolve_citekey
from ..completion import CiteKeyCompletion from ..completion import CiteKeyCompletion
from ..events import NoteEvent
def parser(subparsers, conf): def parser(subparsers, conf):
@ -19,4 +20,5 @@ def command(conf, args):
citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True) citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True)
notepath = rp.databroker.real_notepath(citekey, rp.conf['main']['note_extension']) notepath = rp.databroker.real_notepath(citekey, rp.conf['main']['note_extension'])
ui.edit_file(notepath, temporary=False) ui.edit_file(notepath, temporary=False)
NoteEvent(citekey).send()
rp.close() rp.close()

@ -26,6 +26,7 @@ from .. import pretty
from .. import color from .. import color
from ..utils import resolve_citekey from ..utils import resolve_citekey
from ..completion import CiteKeyOrTagCompletion, TagModifierCompletion from ..completion import CiteKeyOrTagCompletion, TagModifierCompletion
from ..events import TagEvent
def parser(subparsers, conf): def parser(subparsers, conf):
@ -101,7 +102,8 @@ def command(conf, args):
p.add_tag(tag) p.add_tag(tag)
for tag in remove_tags: for tag in remove_tags:
p.remove_tag(tag) p.remove_tag(tag)
rp.push_paper(p, overwrite=True) rp.push_paper(p, overwrite=True, event=False)
TagEvent(citekeyOrTag).send()
elif tags is not None: elif tags is not None:
ui.error(ui.error('No entry found for citekey {}.'.format(citekeyOrTag))) ui.error(ui.error('No entry found for citekey {}.'.format(citekeyOrTag)))
ui.exit() ui.exit()

@ -95,6 +95,27 @@ active = force_list(default=list('alias'))
# command = !pubs list -k | wc -l # command = !pubs list -k | wc -l
# description = lists number of pubs in repo # description = lists number of pubs in repo
[[git]]
# The git plugin will commit changes to the repository in a git repository
# created at the root of the pubs directory. All detected changes will be
# commited every time a change is made by a pubs command.
# The plugin also propose the `pubs git` subcommand, to directory send git
# command to the pubs repository. Therefore, `pubs git status` is equivalent
# to `git -C <pubsdir> status`, with the `-C` flag instructing
# to invoke git as if the current directory was <pubsdir>. Note that a
# limitation of the subcommand is that you cannot use git commands with the
# `-c` option (pubs will interpret it first.)
# if False, will display git output when automatic commit are made.
# Invocation of `pubs git` will always have output displayed.
quiet = boolean(default=True)
# if True, git will not automatically commit changes
manual = boolean(default=False)
# if True, color will be conserved from git output (this add `-c color:always`
# to the git invocation).
force_color = boolean(default=True)
[internal] [internal]
# The version of this configuration file. Do not edit. # The version of this configuration file. Do not edit.
version = string(min=5, default='{}') version = string(min=5, default='{}')

@ -25,17 +25,72 @@ class Event(object):
return wrap return wrap
class RemoveEvent(Event): # Command events
class PreCommandEvent(Event):
description = "Triggered before the command is executed"
class PostCommandEvent(Event):
description = "Triggered after the command is executed"
# Paper changes
class PaperChangeEvent(Event):
_format = "Unspecified modification of paper {citekey}."
def __init__(self, citekey): def __init__(self, citekey):
self.citekey = citekey self.citekey = citekey
@property
def description(self):
return self._format.format(citekey=self.citekey)
# Used by repo.push_paper()
class AddEvent(PaperChangeEvent):
_format = "Added paper {citekey}."
# Used by repo.push_doc()
class DocAddEvent(PaperChangeEvent):
_format = "Added document for {citekey}."
# Used by repo.remove_paper()
class RemoveEvent(PaperChangeEvent):
_format = "Removed paper for {citekey}."
# Used by repo.remove_doc()
class DocRemoveEvent(PaperChangeEvent):
_format = "Removed document for {citekey}."
# Used by commands.tag_cmd.command()
class TagEvent(PaperChangeEvent):
_format = "Updated tags for {citekey}."
# Used by commands.edit_cmd.command()
class ModifyEvent(PaperChangeEvent):
_format = "Modified {file_type} file of {citekey}."
def __init__(self, citekey, file_type):
super(ModifyEvent, self).__init__(citekey)
self.file_type = file_type
@property
def description(self):
return self._format.format(citekey=self.citekey, file_type=self.file_type)
# Used by repo.rename_paper()
class RenameEvent(PaperChangeEvent):
_format = "Renamed paper {old_citekey} to {citekey}."
class RenameEvent(Event):
def __init__(self, paper, old_citekey): def __init__(self, paper, old_citekey):
super(RenameEvent, self).__init__(paper.citekey)
self.paper = paper self.paper = paper
self.old_citekey = old_citekey self.old_citekey = old_citekey
@property
def description(self):
return self._format.format(citekey=self.citekey, old_citekey=self.old_citekey)
class AddEvent(Event): # Used by commands.note_cmd.command()
def __init__(self, citekey): class NoteEvent(PaperChangeEvent):
self.citekey = citekey _format = "Modified note of {citekey}."

@ -27,6 +27,10 @@ class PapersPlugin(object):
else: else:
raise RuntimeError("{} instance not created".format(cls.__name__)) raise RuntimeError("{} instance not created".format(cls.__name__))
@classmethod
def is_loaded(cls):
return cls in _instances
def load_plugins(conf, ui): 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
@ -34,6 +38,8 @@ def load_plugins(conf, ui):
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.
""" """
global _classes, _instances
_classes, _instances = [], {}
for name in conf['plugins']['active']: for name in conf['plugins']['active']:
if len(name) > 0: if len(name) > 0:
modname = '{}.{}.{}.{}'.format('pubs', PLUGIN_NAMESPACE, name, name) modname = '{}.{}.{}.{}'.format('pubs', PLUGIN_NAMESPACE, name, name)
@ -50,7 +56,7 @@ def load_plugins(conf, ui):
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(conf) _instances[obj] = obj(conf, ui)
def get_plugins(): def get_plugins():

@ -65,7 +65,7 @@ class AliasPlugin(PapersPlugin):
name = 'alias' name = 'alias'
def __init__(self, conf): def __init__(self, conf, ui):
self.aliases = [] self.aliases = []
if 'alias' in conf['plugins']: if 'alias' in conf['plugins']:
for name, entry in conf['plugins']['alias'].items(): for name, entry in conf['plugins']['alias'].items():

@ -0,0 +1,117 @@
import os
import sys
import argparse
from subprocess import Popen, PIPE, STDOUT
from pipes import quote as shell_quote
from ... import uis
from ...plugins import PapersPlugin
from ...events import PaperChangeEvent, PostCommandEvent
GITIGNORE = """# files or directories for the git plugin to ignore
.gitignore
.cache/
"""
class GitPlugin(PapersPlugin):
"""The git plugin creates a git repository in the pubs directory and commit the changes
to the pubs repository everytime a paper is modified.
It also add the `pubs git` subcommand, so git commands can be executed in the git repository
from the command line.
"""
name = 'git'
description = "Run git commands in the pubs directory"
def __init__(self, conf, ui):
self.ui = ui
self.pubsdir = os.path.expanduser(conf['main']['pubsdir'])
self.manual = conf['plugins'].get('git', {}).get('manual', False)
self.force_color = conf['plugins'].get('git', {}).get('force_color', True)
self.quiet = conf['plugins'].get('git', {}).get('quiet', True)
self.list_of_changes = []
self._gitinit()
def _gitinit(self):
"""Initialize the git repository if necessary."""
# check that a `.git` directory is present in the pubs dir
git_path = os.path.join(self.pubsdir, '.git')
if not os.path.isdir(git_path):
try:
self.shell('init')
except RuntimeError as exc:
self.ui.error(exc.args[0])
sys.exit(1)
# check that a `.gitignore` file is present
gitignore_path = os.path.join(self.pubsdir, '.gitignore')
if not os.path.isfile(gitignore_path):
with open(gitignore_path, 'w') as fd:
fd.write(GITIGNORE)
def update_parser(self, subparsers, conf):
"""Allow the usage of the pubs git subcommand"""
git_parser = subparsers.add_parser(self.name, help=self.description)
# FIXME: there may be some problems here with the -c argument being ambiguous between
# pubs and git.
git_parser.add_argument('arguments', nargs=argparse.REMAINDER, help="look at man git")
git_parser.set_defaults(func=self.command)
def command(self, conf, args):
"""Execute a git command in the pubs directory"""
self.shell(' '.join([shell_quote(a) for a in args.arguments]), command=True)
def shell(self, cmd, input_stdin=None, command=False):
"""Runs the git program in a shell
:param cmd: the git command, and all arguments, as a single string (e.g. 'add .')
:param input_stdin: if Python 3, must be bytes (i.e., from str, s.encode('utf-8'))
:param command: if True, we're dealing with an explicit `pubs git` invocation.
"""
colorize = ' -c color.ui=always' if self.force_color else ''
git_cmd = 'git -C {}{} {}'.format(self.pubsdir, colorize, cmd)
#print(git_cmd)
p = Popen(git_cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=True)
output, err = p.communicate(input_stdin)
p.wait()
if p.returncode != 0:
raise RuntimeError('The git plugin encountered an error when running the git command:\n' +
'{}\n\nReturned output:\n{}\n'.format(git_cmd, output.decode('utf-8')) +
'If needed, you may fix the state of the {} git repository '.format(self.pubsdir) +
'manually.\nIf relevant, you may submit a bug report at ' +
'https://github.com/pubs/pubs/issues')
elif command:
self.ui.message(output.decode('utf-8'), end='')
elif not self.quiet:
self.ui.info(output.decode('utf-8'))
return output, err, p.returncode
@PaperChangeEvent.listen()
def paper_change_event(event):
"""When a paper is changed, commit the changes to the directory."""
if GitPlugin.is_loaded():
git = GitPlugin.get_instance()
if not git.manual:
event_desc = event.description
for a, b in [('\\','\\\\'), ('"','\\"'), ('$','\\$'), ('`','\\`')]:
event_desc = event_desc.replace(a, b)
git.list_of_changes.append(event_desc)
@PostCommandEvent.listen()
def git_commit(event):
if GitPlugin.is_loaded():
try:
git = GitPlugin.get_instance()
if len(git.list_of_changes) > 0:
if not git.manual:
title = ' '.join(sys.argv) + '\n'
message = '\n'.join([title] + git.list_of_changes)
git.shell('add .')
git.shell('commit -F-', message.encode('utf-8'))
except RuntimeError as exc:
uis.get_ui().warning(exc.args[0])

@ -1,10 +1,13 @@
# PYTHON_ARGCOMPLETE_OK # PYTHON_ARGCOMPLETE_OK
import sys import sys
import argparse
import collections import collections
from . import uis from . import uis
from . import p3 from . import p3
from . import config from . import config
from . import events
from . import commands from . import commands
from . import update from . import update
from . import plugins from . import plugins
@ -38,15 +41,16 @@ CORE_CMDS = collections.OrderedDict([
def execute(raw_args=sys.argv): def execute(raw_args=sys.argv):
try: try:
conf_parser = p3.ArgumentParser(prog="pubs", add_help=False) desc = 'Pubs: your bibliography on the command line.\nVisit https://github.com/pubs/pubs for more information.'
conf_parser.add_argument("-c", "--config", help="path to config file", parser = p3.ArgumentParser(prog="pubs", add_help=False, description=desc)
type=str, metavar="FILE") parser.add_argument("-c", "--config", help="path to an alternate configuration file",
conf_parser.add_argument('--force-colors', dest='force_colors', type=str, metavar="FILE")
action='store_true', default=False, parser.add_argument('--force-colors', dest='force_colors',
help='color are not disabled when piping to a file or other commands') action='store_true', default=False,
#conf_parser.add_argument("-u", "--update", help="update config if needed", help='colors are not disabled when piping to a file or other commands')
# default=False, action='store_true') #parser.add_argument("-u", "--update", help="update config if needed",
top_args, remaining_args = conf_parser.parse_known_args(raw_args[1:]) # default=False, action='store_true')
top_args, remaining_args = parser.parse_known_args(raw_args[1:])
if top_args.config: if top_args.config:
conf_path = top_args.config conf_path = top_args.config
@ -70,10 +74,9 @@ def execute(raw_args=sys.argv):
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()
desc = 'Pubs: your bibliography on the command line.\nVisit https://github.com/pubs/pubs for more information.' parser.add_argument('-v', '--version', action='version', version=__version__)
parser = p3.ArgumentParser(description=desc, parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
prog="pubs", add_help=True) help='Show this help message and exit.')
parser.add_argument('--version', action='version', version=__version__)
subparsers = parser.add_subparsers(title="commands", dest="command") subparsers = parser.add_subparsers(title="commands", dest="command")
# Populate the parser with core commands # Populate the parser with core commands
@ -96,9 +99,12 @@ def execute(raw_args=sys.argv):
parser.print_help(file=sys.stderr) parser.print_help(file=sys.stderr)
sys.exit(2) sys.exit(2)
events.PreCommandEvent().send()
args.prog = "pubs" # FIXME? args.prog = "pubs" # FIXME?
args.func(conf, args) args.func(conf, args)
except Exception as e: except Exception as e:
if not uis.get_ui().handle_exception(e): if not uis.get_ui().handle_exception(e):
raise raise
finally:
events.PostCommandEvent().send()

@ -126,6 +126,7 @@ class Repository(object):
p = self.pull_paper(citekey) p = self.pull_paper(citekey)
p.docpath = None p.docpath = None
self.push_paper(p, overwrite=True, event=False) self.push_paper(p, overwrite=True, event=False)
events.DocRemoveEvent(citekey).send()
except IOError: except IOError:
# FIXME: if IOError is about being unable to # FIXME: if IOError is about being unable to
# remove the file, we need to issue an error.I # remove the file, we need to issue an error.I
@ -191,6 +192,7 @@ class Repository(object):
docfile = system_path(docfile) docfile = system_path(docfile)
p.docpath = docfile p.docpath = docfile
self.push_paper(p, overwrite=True, event=False) self.push_paper(p, overwrite=True, event=False)
events.DocAddEvent(citekey).send()
def unique_citekey(self, base_key, bibentry): def unique_citekey(self, base_key, bibentry):
"""Create a unique citekey for a given base key. """Create a unique citekey for a given base key.

@ -105,6 +105,19 @@ class PrintUI(object):
self.exit() self.exit()
return True # never happens return True # never happens
def test_handle_exception(self, exc):
"""Attempts to handle exception.
:returns: True if exception has been handled (currently never happens)
"""
self.error(ustr(exc))
if DEBUG or self.debug:
raise
else:
self.exit()
return True # never happens
class InputUI(PrintUI): class InputUI(PrintUI):
"""UI class. Stores configuration parameters and system information. """UI class. Stores configuration parameters and system information.

@ -130,6 +130,8 @@ You can access the self-documented configuration by using `pubs conf`, and all t
## Authors ## Authors
### Creators
- [Fabien Benureau](http://fabien.benureau.com) - [Fabien Benureau](http://fabien.benureau.com)
- [Olivier Mangin](http://olivier.mangin.com) - [Olivier Mangin](http://olivier.mangin.com)
@ -141,5 +143,6 @@ You can access the self-documented configuration by using `pubs conf`, and all t
- [Tyler Earnest](https://github.com/tmearnest) - [Tyler Earnest](https://github.com/tmearnest)
- [Dennis Wilson](https://github.com/d9w) - [Dennis Wilson](https://github.com/d9w)
- [Bill Flynn](https://github.com/wflynny) - [Bill Flynn](https://github.com/wflynny)
- [ksunden](https://github.com/ksunden) - [Kyle Sunden](https://github.com/ksunden)
- [Shane Stone](https://github.com/shanewstone) - [Shane Stone](https://github.com/shanewstone)
- [Amlesh Sivanantham](http://github.com/zamlz)

@ -36,7 +36,8 @@ setup(
'pubs.commands', 'pubs.commands',
'pubs.templates', 'pubs.templates',
'pubs.plugs', 'pubs.plugs',
'pubs.plugs.alias'], 'pubs.plugs.alias',
'pubs.plugs.git'],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'pubs=pubs.pubs_cmd:execute', 'pubs=pubs.pubs_cmd:execute',

@ -9,7 +9,7 @@ import dotdot
from pyfakefs import fake_filesystem, fake_filesystem_unittest from pyfakefs import fake_filesystem, fake_filesystem_unittest
from pubs.p3 import input, _fake_stdio, _get_fake_stdio_ucontent from pubs.p3 import input, _fake_stdio, _get_fake_stdio_ucontent
from pubs import content, filebroker from pubs import content, filebroker, uis
# code for fake fs # code for fake fs
@ -20,6 +20,8 @@ real_shutil = shutil
real_glob = glob real_glob = glob
real_io = io real_io = io
original_exception_handler = uis.InputUI.handle_exception
# capture output # capture output
@ -70,6 +72,7 @@ class FakeInput():
self.inputs = list(inputs) or [] self.inputs = list(inputs) or []
self.module_list = module_list self.module_list = module_list
self._cursor = 0 self._cursor = 0
self._original_handler = None
def as_global(self): def as_global(self):
for md in self.module_list: for md in self.module_list:
@ -78,13 +81,11 @@ class FakeInput():
md.InputUI.editor_input = self md.InputUI.editor_input = self
md.InputUI.edit_file = self.input_to_file md.InputUI.edit_file = self.input_to_file
# Do not catch UnexpectedInput # Do not catch UnexpectedInput
original_handler = md.InputUI.handle_exception
def handler(ui, exc): def handler(ui, exc):
if isinstance(exc, self.UnexpectedInput): if isinstance(exc, self.UnexpectedInput):
raise raise
else: else:
original_handler(ui, exc) original_exception_handler(ui, exc)
md.InputUI.handle_exception = handler md.InputUI.handle_exception = handler

@ -0,0 +1,251 @@
from __future__ import print_function, unicode_literals
import os
import sys
import shutil
import tempfile
import unittest
import six
from pubs import pubs_cmd, color, content, uis, p3, events
from pubs.config import conf
from pubs.p3 import _fake_stdio, _get_fake_stdio_ucontent
# makes the tests very noisy
PRINT_OUTPUT = False
CAPTURE_OUTPUT = True
original_exception_handler = uis.InputUI.handle_exception
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 expected to raise SystemExit, catch FakeSystemExit instead.
Added explicit __init__ so SystemExit.code functionality could be emulated.
Taking form from https://stackoverflow.com/a/26938914/1634191
"""
def __init__(self, code=None, *args):
self.code = code
super(FakeSystemExit, self).__init__(
"Exited with code: {}.".format(self.code), *args)
# capture output
def capture(f, verbose=False):
"""Capture the stdout and stderr output.
Useful for comparing the output with the expected one during tests.
:param f: The function to capture output from.
:param verbose: If True, print call will still display their outputs.
If False, they will be silenced.
"""
def newf(*args, **kwargs):
old_stderr, old_stdout = sys.stderr, sys.stdout
sys.stdout = _fake_stdio(additional_out=old_stderr if verbose else None)
sys.stderr = _fake_stdio(additional_out=old_stderr if False else None)
try:
return f(*args, **kwargs), _get_fake_stdio_ucontent(sys.stdout), _get_fake_stdio_ucontent(sys.stderr)
finally:
sys.stderr, sys.stdout = old_stderr, old_stdout
return newf
# scriptable input
class FakeInput():
""" Replace the input() command, and mock user input during tests
Instanciate as :
input = FakeInput(['yes', 'no'])
then replace the input command in every module of the package :
input.as_global()
Then :
input() returns 'yes'
input() returns 'no'
input() raises IndexError
"""
class UnexpectedInput(Exception):
pass
def __init__(self, inputs, module_list=tuple()):
self.inputs = list(inputs) or []
self.module_list = module_list
self._cursor = 0
def as_global(self):
for md in self.module_list:
md.input = self
if md.__name__ == 'pubs.uis':
md.InputUI.editor_input = self
md.InputUI.edit_file = self.input_to_file
def handler(ui, exc):
if isinstance(exc, self.UnexpectedInput):
raise
else:
original_exception_handler(ui, exc)
md.InputUI.handle_exception = handler
def input_to_file(self, path_to_file, temporary=True):
content.write_file(path_to_file, self())
def add_input(self, inp):
self.inputs.append(inp)
def __call__(self, *args, **kwargs):
try:
inp = self.inputs[self._cursor]
self._cursor += 1
return inp
except IndexError:
raise self.UnexpectedInput('Unexpected user input in test.')
class SandboxedCommandTestCase(unittest.TestCase):
maxDiff = 1000000
def setUp(self):
super(SandboxedCommandTestCase, self).setUp()
self.temp_dir = tempfile.mkdtemp()
self.default_pubs_dir = os.path.join(self.temp_dir, 'pubs')
self.default_conf_path = os.path.join(self.temp_dir, 'pubsrc')
os.chdir(os.path.dirname(__file__))
@staticmethod
def _normalize(s):
"""Normalize a string for robust comparisons."""
s = color.undye(s)
try:
s = s.decode('utf-8')
except AttributeError:
pass
return s
def _compare_output(self, s1, s2):
if s1 is not None and s2 is not None:
return self.assertEqual(self._normalize(s1), self._normalize(s2))
def _preprocess_cmd(self, cmd):
"""Sandbox the pubs command into a temporary directory"""
cmd_chunks = cmd.split(' ')
assert cmd_chunks[0] == 'pubs'
prefix = ['pubs', '-c', self.default_conf_path]
if cmd_chunks[1] == 'init':
return ' '.join(prefix + ['init', '-p', self.default_pubs_dir] + cmd_chunks[2:])
else:
return ' '.join(prefix + cmd_chunks[1:])
def execute_cmds(self, cmds, capture_output=CAPTURE_OUTPUT):
""" Execute a list of commands, and capture their output
A command can be a string, or a tuple of size 2, 3 or 4.
In the latter case, the command is :
1. a string reprensenting the command to execute
2. the user inputs to feed to the command during execution
3. the expected output on stdout, verified with assertEqual.
4. the expected output on stderr, verified with assertEqual. (this does not work yet)
"""
try:
outs = []
for cmd in cmds:
inputs = []
expected_out, expected_err = None, None
assert isinstance(cmd, tuple)
actual_cmd = cmd[0]
if len(cmd) >= 2 and cmd[1] is not None: # Inputs provided
inputs = cmd[1]
if len(cmd) >= 3: # Expected output provided
capture_output = True
if cmd[2] is not None:
expected_out = color.undye(cmd[2])
if len(cmd) >= 4 and cmd[3] is not None: # Expected error output provided
expected_err = color.undye(cmd[3])
actual_cmd = self._preprocess_cmd(actual_cmd)
# Always set fake input: test should not ask unexpected user input
input = FakeInput(inputs, [content, uis, p3])
input.as_global()
try:
if capture_output:
execute_captured = capture(pubs_cmd.execute, verbose=PRINT_OUTPUT)
_, stdout, stderr = execute_captured(actual_cmd.split())
self._compare_output(stdout, expected_out)
self._compare_output(stderr, expected_err)
outs.append(self._normalize(stdout))
else:
pubs_cmd.execute(actual_cmd.split())
except FakeInput.UnexpectedInput:
self.fail('Unexpected input asked by command: {}.'.format(actual_cmd))
return outs
except SystemExit as exc:
exc_class, exc, tb = sys.exc_info()
if sys.version_info.major == 2:
# using six to avoid a SyntaxError in Python 3.x
six.reraise(FakeSystemExit, FakeSystemExit(*exc.args), tb)
else:
raise FakeSystemExit(*exc.args).with_traceback(tb)
def tearDown(self):
shutil.rmtree(self.temp_dir, ignore_errors=True)
## Testing the test environments
class TestInput(unittest.TestCase):
"""Test that the fake input mechanisms work correctly in the tests"""
def test_input(self):
input = FakeInput(['yes', 'no'])
self.assertEqual(input(), 'yes')
self.assertEqual(input(), 'no')
with self.assertRaises(FakeInput.UnexpectedInput):
input()
def test_input2(self):
other_input = FakeInput(['yes', 'no'], module_list=[color])
other_input.as_global()
self.assertEqual(color.input(), 'yes')
self.assertEqual(color.input(), 'no')
with self.assertRaises(FakeInput.UnexpectedInput):
color.input()
def test_editor_input(self):
sample_conf = conf.load_default_conf()
ui = uis.InputUI(sample_conf)
other_input = FakeInput(['yes', 'no'], module_list=[uis])
other_input.as_global()
self.assertEqual(ui.editor_input('fake_editor'), 'yes')
self.assertEqual(ui.editor_input('fake_editor'), 'no')
with self.assertRaises(FakeInput.UnexpectedInput):
ui.editor_input()
class TestSandboxedCommandTestCase(SandboxedCommandTestCase):
def test_init_add(self):
"""Simple init and add example"""
correct = ("added to pubs:\n"
"[Page99] Page, Lawrence et al. \"The PageRank Citation Ranking: Bringing Order to the Web.\" (1999) \n")
cmds = [('pubs init',),
('pubs add data/pagerank.bib', [], correct),
#('pubs add abc', [], '', 'error: File does not exist: /Users/self/Volumes/ResearchSync/projects/pubs/abc\n')
]
self.execute_cmds(cmds)
if __name__ == '__main__':
unittest.main(verbosity=2)

@ -0,0 +1,106 @@
import unittest
import subprocess
import sand_env
from pubs import config
def git_hash(pubsdir):
"""Return the git revision"""
hash_cmd = ('git', '-C', pubsdir, 'rev-parse', 'HEAD')
return subprocess.check_output(hash_cmd)
class TestGitPlugin(sand_env.SandboxedCommandTestCase):
def setUp(self, nsec_stat=True):
super(TestGitPlugin, self).setUp()
self.execute_cmds([('pubs init',)])
conf = config.load_conf(path=self.default_conf_path)
conf['plugins']['active'] = ['git']
config.save_conf(conf, path=self.default_conf_path)
def test_git(self):
self.execute_cmds([('pubs add data/pagerank.bib',)])
hash_a = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs add data/pagerank.bib',)])
hash_b = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs rename Page99a ABC',)])
hash_c = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs remove ABC', ['y']),])
hash_d = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs doc add testrepo/doc/Page99.pdf Page99',)])
hash_e = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs doc remove Page99', ['y'])])
hash_f = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs tag Page99 bla+bli',)])
hash_g = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs list',)])
hash_h = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs edit Page99', ['@misc{Page99, title="TTT" author="X. YY"}', 'y',
'@misc{Page99, title="TTT", author="X. YY"}', ''])])
hash_i = git_hash(self.default_pubs_dir)
self.assertNotEqual(hash_a, hash_b)
self.assertNotEqual(hash_b, hash_c)
self.assertNotEqual(hash_c, hash_d)
self.assertNotEqual(hash_d, hash_e)
self.assertNotEqual(hash_e, hash_f)
self.assertNotEqual(hash_f, hash_g)
self.assertEqual(hash_g, hash_h)
self.assertNotEqual(hash_h, hash_i)
# # basically can't test that because each command is not completely independent in
# # SandoboxedCommands.
# # will work if we use subprocess.
# conf = config.load_conf(path=self.default_conf_path)
# conf['plugins']['active'] = []
# config.save_conf(conf, path=self.default_conf_path)
#
# self.execute_cmds([('pubs add data/pagerank.bib',)])
# hash_j = git_hash(self.default_pubs_dir)
#
# self.assertEqual(hash_i, hash_j)
def test_manual(self):
conf = config.load_conf(path=self.default_conf_path)
conf['plugins']['active'] = ['git']
conf['plugins']['git']['manual'] = True
config.save_conf(conf, path=self.default_conf_path)
# this three lines just to initialize the git HEAD
self.execute_cmds([('pubs add data/pagerank.bib',)])
self.execute_cmds([('pubs git add .',)])
self.execute_cmds([('pubs git commit -m "initial_commit"',)])
self.execute_cmds([('pubs add data/pagerank.bib',)])
hash_j = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs add data/pagerank.bib',)])
hash_k = git_hash(self.default_pubs_dir)
self.assertEqual(hash_j, hash_k)
self.execute_cmds([('pubs git add .',)])
hash_l = git_hash(self.default_pubs_dir)
self.assertEqual(hash_k, hash_l)
self.execute_cmds([('pubs git commit -m "abc"',)])
hash_m = git_hash(self.default_pubs_dir)
self.assertNotEqual(hash_l, hash_m)
if __name__ == '__main__':
unittest.main()

@ -69,11 +69,11 @@ class AliasPluginTestCase(unittest.TestCase):
self.conf['plugins']['active'] = ['alias'] self.conf['plugins']['active'] = ['alias']
def testAliasPluginCreated(self): def testAliasPluginCreated(self):
self.plugin = AliasPlugin(self.conf) self.plugin = AliasPlugin(self.conf, None)
def testAliasPluginOneCommnand(self): def testAliasPluginOneCommnand(self):
self.conf['plugins']['alias'] = {'print': 'open -w lpppp'} self.conf['plugins']['alias'] = {'print': 'open -w lpppp'}
self.plugin = AliasPlugin(self.conf) self.plugin = AliasPlugin(self.conf, None)
self.assertEqual(len(self.plugin.aliases), 1) self.assertEqual(len(self.plugin.aliases), 1)
self.assertEqual(type(self.plugin.aliases[0]), CommandAlias) self.assertEqual(type(self.plugin.aliases[0]), CommandAlias)
self.assertEqual(self.plugin.aliases[0].name, 'print') self.assertEqual(self.plugin.aliases[0].name, 'print')
@ -81,7 +81,7 @@ class AliasPluginTestCase(unittest.TestCase):
def testAliasPluginOneShell(self): def testAliasPluginOneShell(self):
self.conf['plugins']['alias'] = {'count': '!pubs list -k | wc -l'} self.conf['plugins']['alias'] = {'count': '!pubs list -k | wc -l'}
self.plugin = AliasPlugin(self.conf) self.plugin = AliasPlugin(self.conf, None)
self.assertEqual(len(self.plugin.aliases), 1) self.assertEqual(len(self.plugin.aliases), 1)
self.assertEqual(type(self.plugin.aliases[0]), ShellAlias) self.assertEqual(type(self.plugin.aliases[0]), ShellAlias)
self.assertEqual(self.plugin.aliases[0].name, 'count') self.assertEqual(self.plugin.aliases[0].name, 'count')
@ -91,13 +91,13 @@ class AliasPluginTestCase(unittest.TestCase):
def testAliasPluginTwoCommnands(self): def testAliasPluginTwoCommnands(self):
self.conf['plugins']['alias'] = {'print': 'open -w lpppp', self.conf['plugins']['alias'] = {'print': 'open -w lpppp',
'count': '!pubs list -k | wc -l'} 'count': '!pubs list -k | wc -l'}
self.plugin = AliasPlugin(self.conf) self.plugin = AliasPlugin(self.conf, None)
self.assertEqual(len(self.plugin.aliases), 2) self.assertEqual(len(self.plugin.aliases), 2)
def testAliasPluginNestedDefinitionType(self): def testAliasPluginNestedDefinitionType(self):
self.conf['plugins']['alias'] = {'print': {'description': 'print this', self.conf['plugins']['alias'] = {'print': {'description': 'print this',
'command': 'open -w lpppp'}} 'command': 'open -w lpppp'}}
self.plugin = AliasPlugin(self.conf) self.plugin = AliasPlugin(self.conf, None)
self.assertEqual(len(self.plugin.aliases), 1) self.assertEqual(len(self.plugin.aliases), 1)
self.assertEqual(type(self.plugin.aliases[0]), CommandAlias) self.assertEqual(type(self.plugin.aliases[0]), CommandAlias)
self.assertEqual(self.plugin.aliases[0].name, 'print') self.assertEqual(self.plugin.aliases[0].name, 'print')
@ -106,7 +106,7 @@ class AliasPluginTestCase(unittest.TestCase):
def testAliasPluginNestedDefinitionNoDescription(self): def testAliasPluginNestedDefinitionNoDescription(self):
self.conf['plugins']['alias'] = {'print': {'command': 'open -w lpppp'}} self.conf['plugins']['alias'] = {'print': {'command': 'open -w lpppp'}}
self.plugin = AliasPlugin(self.conf) self.plugin = AliasPlugin(self.conf, None)
self.assertEqual(len(self.plugin.aliases), 1) self.assertEqual(len(self.plugin.aliases), 1)
self.assertEqual(type(self.plugin.aliases[0]), CommandAlias) self.assertEqual(type(self.plugin.aliases[0]), CommandAlias)
self.assertEqual(self.plugin.aliases[0].name, 'print') self.assertEqual(self.plugin.aliases[0].name, 'print')
@ -118,7 +118,7 @@ class AliasPluginTestCase(unittest.TestCase):
self.conf['plugins']['alias'] = {'print': {'description': 'print this', self.conf['plugins']['alias'] = {'print': {'description': 'print this',
'command': 'open -w lpppp'}, 'command': 'open -w lpppp'},
'count': '!pubs list -k | wc -l'} 'count': '!pubs list -k | wc -l'}
self.plugin = AliasPlugin(self.conf) self.plugin = AliasPlugin(self.conf, None)
self.plugin.aliases = sorted(self.plugin.aliases, key=lambda a: a.name) self.plugin.aliases = sorted(self.plugin.aliases, key=lambda a: a.name)
self.assertEqual(len(self.plugin.aliases), 2) self.assertEqual(len(self.plugin.aliases), 2)
@ -139,7 +139,7 @@ class AliasPluginTestCase(unittest.TestCase):
self.conf['plugins']['alias'] = {'print': {'description': 'print this', self.conf['plugins']['alias'] = {'print': {'description': 'print this',
'command': 'open -w lpppp', 'command': 'open -w lpppp',
'count': '!pubs list -k | wc -l'}} 'count': '!pubs list -k | wc -l'}}
self.plugin = AliasPlugin(self.conf) self.plugin = AliasPlugin(self.conf, None)
self.assertEqual(len(self.plugin.aliases), 1) self.assertEqual(len(self.plugin.aliases), 1)
self.assertEqual(type(self.plugin.aliases[0]), CommandAlias) self.assertEqual(type(self.plugin.aliases[0]), CommandAlias)
@ -147,3 +147,8 @@ class AliasPluginTestCase(unittest.TestCase):
self.assertEqual(self.plugin.aliases[0].name, 'print') self.assertEqual(self.plugin.aliases[0].name, 'print')
self.assertEqual(self.plugin.aliases[0].description, 'print this') self.assertEqual(self.plugin.aliases[0].description, 'print this')
self.assertEqual(self.plugin.aliases[0].definition, 'open -w lpppp') self.assertEqual(self.plugin.aliases[0].definition, 'open -w lpppp')
if __name__ == '__main__':
unittest.main()

Loading…
Cancel
Save