Merge branch 'feat/config' into develop

New config interface.
Mostly rewritten repo class.
Version support.
main
Fabien Benureau 12 years ago
commit 50e7d1bdab

@ -15,10 +15,12 @@ Git
This project use git-flow {nvie.com/posts/a-successful-git-branching-model} This project use git-flow {nvie.com/posts/a-successful-git-branching-model}
as a model for branches management. In particular : as a model for branches management. In particular :
- master is for release only 1. master is for release only
- when you commit to develop, run nosetests before. All tests should pass. 2. when you commit to develop, run nosetests before. All tests should pass.
- in feature/branches, you do whatever you want. 3. when you commit to develop, run nosetests before. All tests should pass.
- when developping a new feature, write tests for it. 4. when you commit to develop, run nosetests before. All tests should pass.
5. in 'feat/' branches, you do whatever you want.
6. when developping a new feature, write tests for it.
Alignement Alignement
@ -52,7 +54,7 @@ Names
Avoid at all cost to name a variable like a module from the package, a Avoid at all cost to name a variable like a module from the package, a
dependency or the standart lib. dependency or the standart lib.
This breaks coherence across the code, makes it harder to read. This breaks coherence across the code, makes it harder to read.
Change either the module or variable name, I don't care. Change either the module or variable name.
Function that have only local uses should be preceded by an underscore. Function that have only local uses should be preceded by an underscore.
@ -66,3 +68,14 @@ These functiona won't be imported automatically with the module.
It keeps the interface clean, makes occasional hacks explicit, and inform other It keeps the interface clean, makes occasional hacks explicit, and inform other
developers that theses functions may need special care when uses outside their developers that theses functions may need special care when uses outside their
natural habitat. natural habitat.
Files
=====
Unless you have a good reason, use 'open' as such :
yes: with open(path, 'w') as f:
f.read()
no : f = open(path, 'r')
f.read()
f.close()

@ -0,0 +1 @@
__version__ = 3

@ -1,11 +1,11 @@
from .. import repo from .. import repo
from .. import files from .. import files
from ..paper import Paper, NoDocumentFile, get_bibentry_from_string from ..paper import Paper, NoDocumentFile, get_bibentry_from_string
from .. import configs from ..configs import config
from .helpers import add_paper_with_docfile, extract_doc_path_from_bibdata from .helpers import add_paper_with_docfile, extract_doc_path_from_bibdata
def parser(subparsers, config): def parser(subparsers):
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('-b', '--bibfile', parser.add_argument('-b', '--bibfile',
help='bibtex, bibtexml or bibyaml file', default=None) help='bibtex, bibtexml or bibyaml file', default=None)
@ -19,14 +19,14 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, bibfile, docfile, tags, copy): def command(ui, bibfile, docfile, tags, copy):
""" """
: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
""" """
if copy is None: if copy is None:
copy = config.get(configs.MAIN_SECTION, 'import-copy') copy = config().import_copy
rp = repo.Repository.from_directory(config) rp = repo.Repository(config())
if bibfile is None: if bibfile is None:
cont = True cont = True
bibstr = '' bibstr = ''

@ -1,18 +1,18 @@
from .. import repo from .. import repo
from ..paper import Paper from ..paper import Paper
from ..configs import config
def parser(subparsers):
def parser(subparsers, config):
parser = subparsers.add_parser('add_library', parser = subparsers.add_parser('add_library',
help='add a set of papers to the repository') help='add a set of papers to the repository')
parser.add_argument('bibfile', help='bibtex, bibtexml or bibyaml file') parser.add_argument('bibfile', help='bibtex, bibtexml or bibyaml file')
return parser return parser
def command(config, ui, bibfile): def command(ui, bibfile):
""" """
:param bibtex bibtex file (in .bib, .bibml or .yaml format. :param bibtex bibtex file (in .bib, .bibml or .yaml format.
""" """
rp = repo.Repository.from_directory(config) rp = repo.Repository.from_directory(config())
for p in Paper.many_from_bib(bibfile): for p in Paper.many_from_bib(bibfile):
rp.add_paper(p) rp.add_paper(p)

@ -1,10 +1,10 @@
from .. import repo from .. import repo
from .. import configs from ..configs import config
from .helpers import (add_references_argument, parse_reference, from .helpers import (add_references_argument, parse_reference,
add_docfile_to_paper) add_docfile_to_paper)
def parser(subparsers, config): def parser(subparsers):
parser = subparsers.add_parser('attach', parser = subparsers.add_parser('attach',
help='attach a document to an existing paper') help='attach a document to an existing paper')
parser.add_argument('-c', '--copy', action='store_true', default=None, parser.add_argument('-c', '--copy', action='store_true', default=None,
@ -16,14 +16,14 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, copy, reference, document): def command(ui, copy, reference, document):
""" """
: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
""" """
if copy is None: if copy is None:
copy = config.get(configs.MAIN_SECTION, 'import-copy') copy = config().import_copy
rp = repo.Repository.from_directory(config) rp = repo.Repository(config())
key = parse_reference(ui, rp, reference) key = parse_reference(ui, rp, reference)
paper = rp.get_paper(key) paper = rp.get_paper(key)
try: try:

@ -2,9 +2,10 @@ from ..files import editor_input
from .. import repo from .. import repo
from ..paper import get_bibentry_from_string, get_safe_metadata_from_content from ..paper import get_bibentry_from_string, get_safe_metadata_from_content
from .helpers import add_references_argument, parse_reference from .helpers import add_references_argument, parse_reference
from ..configs import config
def parser(subparsers, config): def parser(subparsers):
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,
@ -13,8 +14,8 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, meta, reference): def command(ui, meta, reference):
rp = repo.Repository.from_directory(config) rp = repo.Repository.from_directory(config())
key = parse_reference(ui, rp, reference) key = parse_reference(ui, rp, reference)
paper = rp.get_paper(key) paper = rp.get_paper(key)
to_edit = 'bib' to_edit = 'bib'
@ -25,7 +26,7 @@ def command(config, ui, meta, reference):
content = f.read() content = f.read()
while True: while True:
# Get new content from user # Get new content from user
content = editor_input(config, content) content = editor_input(config().edit_cmd, content)
new_key = key new_key = key
bib = None bib = None
metadata = None metadata = None

@ -5,9 +5,9 @@ from pybtex.database import BibliographyData
from .. import repo from .. import repo
from .. import files from .. import files
from .helpers import parse_references, add_references_argument from .helpers import parse_references, add_references_argument
from ..configs import config
def parser(subparsers):
def parser(subparsers, config):
parser = subparsers.add_parser('export', parser = subparsers.add_parser('export',
help='export bibliography') help='export bibliography')
parser.add_argument('-f', '--bib-format', default='bibtex', parser.add_argument('-f', '--bib-format', default='bibtex',
@ -16,11 +16,11 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, bib_format, references): def command(ui, bib_format, references):
""" """
:param bib_format (in 'bibtex', 'yaml') :param bib_format (in 'bibtex', 'yaml')
""" """
rp = repo.Repository.from_directory(config) rp = repo.Repository.from_directory(config())
papers = [rp.get_paper(c) papers = [rp.get_paper(c)
for c in parse_references(ui, rp, references)] for c in parse_references(ui, rp, references)]
if len(papers) == 0: if len(papers) == 0:

@ -42,7 +42,7 @@ def extract_doc_path_from_bibdata(paper, ui):
def parse_reference(ui, rp, ref): def parse_reference(ui, rp, ref):
try: try:
return rp.citekey_from_ref(ref) return rp.ref2citekey(ref)
except InvalidReference: except InvalidReference:
ui.error("no paper with reference: %s." ui.error("no paper with reference: %s."
% color.dye(ref, color.citekey)) % color.dye(ref, color.citekey))

@ -1,10 +1,9 @@
from .. import repo from .. import repo
from ..paper import Paper from ..paper import Paper
from .helpers import add_paper_with_docfile, extract_doc_path_from_bibdata from .helpers import add_paper_with_docfile, extract_doc_path_from_bibdata
from .. import configs from ..configs import config
def parser(subparsers):
def parser(subparsers, config):
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',
@ -16,13 +15,13 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, bibpath, copy): def command(ui, bibpath, copy):
""" """
:param bibpath: path (no url yet) to a bibliography file :param bibpath: path (no url yet) to a bibliography file
""" """
if copy is None: if copy is None:
copy = config.get(configs.MAIN_SECTION, 'import-copy') copy = config().import_copy
rp = repo.Repository.from_directory(config) rp = repo.Repository.from_directory(config())
# Extract papers from bib # Extract papers from bib
papers = Paper.many_from_path(bibpath, fatal=False) papers = Paper.many_from_path(bibpath, fatal=False)
for p in papers: for p in papers:

@ -3,10 +3,11 @@
import os import os
from ..repo import Repository from ..repo import Repository
from .. import configs from ..configs import config
from .. import color from .. import color
from .. import files
def parser(subparsers, config): def parser(subparsers):
parser = subparsers.add_parser('init', parser = subparsers.add_parser('init',
help="initialize the papers directory") help="initialize the papers directory")
parser.add_argument('-p', '--path', default=None, parser.add_argument('-p', '--path', default=None,
@ -17,21 +18,19 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, path, doc_dir): def command(ui, path, doc_dir):
"""Create a .papers directory""" """Create a .papers directory"""
if path is None: if path is not None:
papersdir = config.get('papers', 'papers-directory') config().papers_dir = files.clean_path(os.getcwd(), path)
else: ppd = config().papers_dir
papersdir = os.path.join(os.getcwd(), path) if os.path.exists(ppd) and len(os.listdir(ppd)) > 0:
configs.add_and_write_option('papers', 'papers-directory', papersdir)
if os.path.exists(papersdir):
if len(os.listdir(papersdir)) > 0:
ui.error('directory {} is not empty.'.format( ui.error('directory {} is not empty.'.format(
color.dye(papersdir, color.filepath))) color.dye(ppd, color.filepath)))
ui.exit() ui.exit()
ui.print_('Initializing papers in {}.'.format( ui.print_('Initializing papers in {}.'.format(
color.dye(papersdir, color.filepath))) color.dye(ppd, color.filepath)))
repo = Repository()
repo.init(papersdir) # Creates directories repo = Repository(config(), load = False)
repo.save() # Saves empty repository description repo.save()
config().save()

@ -2,9 +2,10 @@ from .. import pretty
from .. import repo from .. import repo
from .. import color from .. import color
from . import helpers from . import helpers
from ..configs import config
def parser(subparsers, config): def parser(subparsers):
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',
@ -14,8 +15,8 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, citekeys, query): def command(ui, citekeys, query):
rp = repo.Repository.from_directory(config) rp = repo.Repository(config())
papers = [(n, p) for n, p in enumerate(rp.all_papers()) papers = [(n, p) for n, p in enumerate(rp.all_papers())
if test_paper(query, p)] if test_paper(query, p)]
ui.print_('\n'.join(helpers.paper_oneliner(p, n = n, citekey_only = citekeys) for n, p in papers)) ui.print_('\n'.join(helpers.paper_oneliner(p, n = n, citekey_only = citekeys) for n, p in papers))

@ -2,12 +2,12 @@ import subprocess
from .. import repo from .. import repo
from ..paper import NoDocumentFile from ..paper import NoDocumentFile
from .. import configs from ..configs import config
from .. import color from .. import color
from .helpers import add_references_argument, parse_reference from .helpers import add_references_argument, parse_reference
def parser(subparsers, config): def parser(subparsers):
parser = subparsers.add_parser('open', parser = subparsers.add_parser('open',
help='open the paper in a pdf viewer') help='open the paper in a pdf viewer')
parser.add_argument('-w', '--with', dest='with_command', default=None, parser.add_argument('-w', '--with', dest='with_command', default=None,
@ -16,12 +16,12 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, with_command, reference): def command(ui, with_command, reference):
rp = repo.Repository.from_directory(config) rp = repo.Repository.from_directory(config)
key = parse_reference(ui, rp, reference) key = parse_reference(ui, rp, reference)
paper = rp.get_paper(key) paper = rp.get_paper(key)
if with_command is None: if with_command is None:
with_command = config.get(configs.MAIN_SECTION, 'open-cmd') with_command = config().open_cmd
try: try:
filepath = paper.get_document_path() filepath = paper.get_document_path()
subprocess.Popen([with_command, filepath]) subprocess.Popen([with_command, filepath])

@ -1,12 +1,12 @@
from .. import repo from .. import repo
from .. import color from .. import color
from .. import configs from ..configs import config
from .helpers import add_references_argument, parse_references from .helpers import add_references_argument, parse_references
from ..events import RemoveEvent from ..events import RemoveEvent
def parser(subparsers, config): def parser(subparsers):
parser = subparsers.add_parser('remove', help='removes a paper') parser = subparsers.add_parser('remove', help='removes a paper')
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.")
@ -14,8 +14,8 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, force, references): def command(ui, force, references):
rp = repo.Repository.from_directory(config) rp = repo.Repository(config())
citekeys = parse_references(ui, rp, references) citekeys = parse_references(ui, rp, references)
if force is None: if force is None:
are_you_sure = ("Are you sure you want to delete paper(s) [%s]" are_you_sure = ("Are you sure you want to delete paper(s) [%s]"
@ -24,7 +24,7 @@ def command(config, ui, force, references):
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 citekeys: for c in citekeys:
rmevent = RemoveEvent(config, ui, c) rmevent = RemoveEvent(ui, c)
rmevent.send() rmevent.send()
rp.remove(c) rp.remove_paper(c)

@ -19,8 +19,9 @@ The different use cases are :
from ..repo import Repository, InvalidReference from ..repo import Repository, InvalidReference
from . import helpers from . import helpers
from ..configs import config
def parser(subparsers, config): def parser(subparsers):
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('referenceOrTag', nargs='?', default = None, parser.add_argument('referenceOrTag', nargs='?', default = None,
help='reference to the paper (citekey or number), or ' help='reference to the paper (citekey or number), or '
@ -43,12 +44,12 @@ def _parse_tags(s):
for m in re.finditer(r'[+-]', s): for m in re.finditer(r'[+-]', s):
if m.start() == last: if m.start() == last:
if last != 0: if last != 0:
raise ValueError, 'could not match tag expression' raise ValueError('could not match tag expression')
else: else:
tags.append(s[last:(m.start())]) tags.append(s[last:(m.start())])
last = m.start() last = m.start()
if last == len(s): if last == len(s):
raise ValueError, 'could not match tag expression' raise ValueError('could not match tag expression')
else: else:
tags.append(s[last:]) tags.append(s[last:])
return tags return tags
@ -63,16 +64,16 @@ 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(config, ui, referenceOrTag, tags): def command(ui, referenceOrTag, tags):
"""Add, remove and show tags""" """Add, remove and show tags"""
rp = Repository.from_directory(config) rp = Repository(config())
if referenceOrTag is None: if referenceOrTag is None:
for tag in rp.get_tags(): for tag in rp.get_tags():
ui.print_(tag) ui.print_(tag)
else: else:
try: try:
citekey = rp.citekey_from_ref(referenceOrTag) citekey = rp.ref2citekey(referenceOrTag)
p = rp.get_paper(citekey) p = rp.get_paper(citekey)
if tags is None: if tags is None:
ui.print_(' '.join(p.tags)) ui.print_(' '.join(p.tags))

@ -1,19 +1,60 @@
import sys
from .. import repo from .. import repo
from .. import color from .. import color
from ..configs import config
from ..__init__ import __version__
def parser(subparsers, config): def parser(subparsers):
parser = subparsers.add_parser('update', help='update the repository to the lastest format') parser = subparsers.add_parser('update', help='update the repository to the lastest format')
return parser return parser
def command(config, ui): def command(ui):
rp = repo.Repository.from_directory(config) code_version = __version__
msg = ("You should backup the paper directory {} before continuing." repo_version = int(config().version)
"Continue ?").format(color.dye(rp.papersdir, color.filepath))
sure = ui.input_yn(question=msg, default='n') if repo_version == code_version:
if sure: ui.print_('You papers repository is up-to-date.')
for p in rp.all_papers(): sys.exit(0)
tags = set(p.metadata['tags']) elif repo_version > code_version:
tags = tags.union(p.metadata['labels']) ui.print_('Your repository was generated with an newer version of papers.\n'
p.metadata.pop('labels', []) 'You should not use papers until you install the newest version.')
rp.save_paper(p) sys.exit(0)
else:
msg = ("You should backup the paper directory {} before continuing."
"Continue ?").format(color.dye(config().papers_dir, color.filepath))
sure = ui.input_yn(question=msg, default='n')
if not sure:
sys.exit(0)
if repo_version == 1:
rp = repo.Repository(config())
for p in rp.all_papers():
tags = set(p.metadata['tags'])
tags = tags.union(p.metadata['labels'])
p.metadata.pop('labels', [])
rp.save_paper(p)
repo_version = 2
if repo_version == 2:
# update config
print 'bla'
cfg_update = [('papers-directory', 'papers_dir'),
('open-cmd', 'open_cmd'),
('edit-cmd', 'edit_cmd'),
('import-copy', 'import_copy'),
('import-move', 'import_move'),
]
for old, new in cfg_update:
try:
config()._cfg.set('papers', new, config()._cfg.get('papers', old))
config()._cfg.remove_option('papers', old)
except Exception:
pass
config().save()
repo_version = 3
config().version = repo_version
config().save()

@ -2,7 +2,7 @@ import webbrowser
import urllib import urllib
def parser(subparsers, config): def parser(subparsers):
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 = '*',
@ -10,7 +10,7 @@ def parser(subparsers, config):
return parser return parser
def command(config, ui, search_string): def command(ui, search_string):
print search_string print search_string
url = ("https://scholar.google.fr/scholar?q=%s&lr=" url = ("https://scholar.google.fr/scholar?q=%s&lr="
% (urllib.quote_plus(' '.join(search_string)))) % (urllib.quote_plus(' '.join(search_string))))

@ -1,59 +1,92 @@
import os import os
import ConfigParser import copy
from p3 import configparser
# constant stuff (DFT = DEFAULT)
MAIN_SECTION = 'papers' MAIN_SECTION = 'papers'
CONFIG_PATH = os.path.expanduser('~/.papersrc') DFT_CONFIG_PATH = os.path.expanduser('~/.papersrc')
DEFAULT_PAPERS_DIRECTORY = os.path.expanduser('~/.papers')
DEFAULT_OPEN_CMD = 'open'
try: try:
DEFAULT_EDIT_CMD = os.environ['EDITOR'] DFT_EDIT_CMD = os.environ['EDITOR']
except KeyError: except KeyError:
DEFAULT_EDIT_CMD = 'vi' DFT_EDIT_CMD = 'vi'
DEFAULT_IMPORT_COPY = 'yes' DFT_PLUGINS = 'texnote'
DEFAULT_IMPORT_MOVE = 'no'
DEFAULT_COLOR = 'yes'
DEFAULT_PLUGINS = 'texnote'
CONFIG = ConfigParser.SafeConfigParser({ DFT_CONFIG = {'papers_dir' : os.path.expanduser('~/.papers'),
'papers-directory': DEFAULT_PAPERS_DIRECTORY, 'doc_dir' : 'doc',
'open-cmd': DEFAULT_OPEN_CMD, 'import_copy' : 'yes',
'edit-cmd': DEFAULT_EDIT_CMD, 'import_move' : 'no',
'import-copy': DEFAULT_IMPORT_COPY, 'color' : 'yes',
'import-move': DEFAULT_IMPORT_MOVE, 'version' : 3,
'color': DEFAULT_COLOR,
'plugins': DEFAULT_PLUGINS})
CONFIG.add_section(MAIN_SECTION)
'open_cmd' : 'open',
'edit_cmd' : DFT_EDIT_CMD,
'plugins' : DFT_PLUGINS
}
def read_config(): BOOLEANS = {'import_copy', 'import_move', 'color'}
CONFIG.read(CONFIG_PATH)
return CONFIG
def add_and_write_option(section, option, value): # package-shared config that can be accessed using :
cfg = ConfigParser.ConfigParser() # from configs import config
cfg.read(CONFIG_PATH) _config = None
if not cfg.has_section(section): def config(section = MAIN_SECTION):
cfg.add_section(section) if _config is None:
raise ValueError, 'not config instanciated yet'
_config._section = section
return _config
cfg.set(section, option, value) class Config(object):
f = open(CONFIG_PATH, 'w') def __init__(self, **kwargs):
cfg.write(f) object.__setattr__(self, '_section', MAIN_SECTION) # active section
f.close() object.__setattr__(self, '_cfg', configparser.SafeConfigParser())
self._cfg.add_section(self._section)
for name, value in DFT_CONFIG.items():
self._cfg.set(self._section, name, str(value))
def get_plugins(cfg): for name, value in kwargs.items():
return cfg.get(MAIN_SECTION, 'plugins').split() self.__setattr__(name, value)
def as_global(self):
global _config
_config = self
def get_boolean(value, default = False): def load(self, path = DFT_CONFIG_PATH):
value = str(value).lower() self._cfg.read(path)
if value in ('yes', 'true', 't', 'y', '1'): return self
return True
elif value in ('no', 'false', 'f', 'n', '0'): def save(self, path = DFT_CONFIG_PATH):
return False with open(path, 'w') as f:
else: self._cfg.write(f)
return default
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,7 +28,6 @@ class Event(object):
class RemoveEvent(Event): class RemoveEvent(Event):
def __init__(self, config, ui, citekey): def __init__(self, ui, citekey):
self.config = config
self.ui = ui self.ui = ui
self.citekey = citekey self.citekey = citekey

@ -1,3 +1,9 @@
"""
This module can't depend on configs.
If you feel the need to import configs, you are not in the right place.
"""
import os import os
import subprocess import subprocess
import tempfile import tempfile
@ -6,7 +12,6 @@ from cStringIO import StringIO
import yaml import yaml
from . import ui from . import ui
from . import configs
from . import color from . import color
try: try:
@ -30,21 +35,21 @@ except ImportError:
_papersdir = None _papersdir = None
BIB_EXTENSIONS = ['.bib', '.bibyaml', '.bibml', '.yaml'] BIB_EXTENSIONS = ['.bib', '.bibyaml', '.bibml', '.yaml']
FORMATS_INPUT = {'bib': pybtex.database.input.bibtex, FORMATS_INPUT = {'bib' : pybtex.database.input.bibtex,
'xml': pybtex.database.input.bibtexml, 'xml' : pybtex.database.input.bibtexml,
'yml': pybtex.database.input.bibyaml, 'yml' : pybtex.database.input.bibyaml,
'yaml': pybtex.database.input.bibyaml, 'yaml' : pybtex.database.input.bibyaml,
'bibyaml': pybtex.database.input.bibyaml} 'bibyaml': pybtex.database.input.bibyaml}
FORMATS_OUTPUT = {'bib': pybtex.database.output.bibtex, FORMATS_OUTPUT = {'bib' : pybtex.database.output.bibtex,
'bibtex': pybtex.database.output.bibtex, 'bibtex' : pybtex.database.output.bibtex,
'xml': pybtex.database.output.bibtexml, 'xml' : pybtex.database.output.bibtexml,
'yml': pybtex.database.output.bibyaml, 'yml' : pybtex.database.output.bibyaml,
'yaml': pybtex.database.output.bibyaml, 'yaml' : pybtex.database.output.bibyaml,
'bibyaml': pybtex.database.output.bibyaml} 'bibyaml': pybtex.database.output.bibyaml}
def clean_path(path): def clean_path(*args):
return os.path.abspath(os.path.expanduser(path)) return os.path.abspath(os.path.expanduser(os.path.join(*args)))
def name_from_path(fullpdfpath, verbose=False): def name_from_path(fullpdfpath, verbose=False):
@ -168,11 +173,10 @@ def parse_bibdata(content, format_ = None):
raise ValueError, 'content format is not recognized.' raise ValueError, 'content format is not recognized.'
def editor_input(config, initial="", suffix=None): def editor_input(editor, initial="", suffix=None):
"""Use an editor to get input""" """Use an editor to get input"""
if suffix is None: if suffix is None:
suffix = '.tmp' suffix = '.tmp'
editor = config.get(configs.MAIN_SECTION, 'edit-cmd')
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file: with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file:
tfile_name = temp_file.name tfile_name = temp_file.name
temp_file.write(initial) temp_file.write(initial)

@ -166,7 +166,7 @@ class Paper(object):
citekey = u'{}{}'.format(u''.join(first_author.last()), year) citekey = u'{}{}'.format(u''.join(first_author.last()), year)
return str2citekey(citekey) return str2citekey(citekey)
def save_to_disc(self, bib_filepath, meta_filepath): def save(self, bib_filepath, meta_filepath):
"""Creates a BibliographyData object containing a single entry and """Creates a BibliographyData object containing a single entry and
saves it to disc. saves it to disc.
""" """

@ -28,11 +28,15 @@ cmds = collections.OrderedDict([
def execute(raw_args = sys.argv): def execute(raw_args = sys.argv):
config = configs.read_config() # loading config
config = configs.Config()
config.load()
config.as_global()
ui = UI(config) ui = UI(config)
# Extend with plugin commands # Extend with plugin commands
plugin.load_plugins(config, ui, configs.get_plugins(config)) plugin.load_plugins(ui, config.plugins.split())
for p in plugin.get_plugins().values(): for p in plugin.get_plugins().values():
cmds.update(collections.OrderedDict([(p.name, p)])) cmds.update(collections.OrderedDict([(p.name, p)]))
@ -40,10 +44,9 @@ def execute(raw_args = sys.argv):
subparsers = parser.add_subparsers(title="valid commands", dest="command") subparsers = parser.add_subparsers(title="valid commands", dest="command")
for cmd_mod in cmds.values(): for cmd_mod in cmds.values():
subparser = cmd_mod.parser(subparsers, config) # why do we return the subparser ? subparser = cmd_mod.parser(subparsers) # why do we return the subparser ?
args = parser.parse_args(raw_args[1:]) args = parser.parse_args(raw_args[1:])
args.config = config
args.ui = ui args.ui = ui
cmd = args.command cmd = args.command

@ -1,4 +1,5 @@
import importlib import importlib
from .configs import config
PLUGIN_NAMESPACE = 'plugs' PLUGIN_NAMESPACE = 'plugs'
@ -11,20 +12,19 @@ class PapersPlugin(object):
functionality by defining a subclass of PapersPlugin and overriding functionality by defining a subclass of PapersPlugin and overriding
the abstract methods defined here. the abstract methods defined here.
""" """
def __init__(self, config, ui): def __init__(self, ui):
"""Perform one-time plugin setup. """Perform one-time plugin setup.
""" """
self.name = self.__module__.split('.')[-1] self.name = self.__module__.split('.')[-1]
self.config = config
self.ui = ui self.ui = ui
#config and ui and given again to stay consistent with the core papers cmd. #ui and given again to stay consistent with the core papers cmd.
#two options: #two options:
#- create specific cases in script papers/papers #- create specific cases in script papers/papers
#- do not store self.config and self.ui and use them if needed when command is called #- do not store self.ui and use them if needed when command is called
#this may end up with a lot of function with config/ui in argument #this may end up with a lot of function with config/ui in argument
#or just keep it that way... #or just keep it that way...
def parser(self, subparsers, config): def parser(self, subparsers):
""" Should retrun the parser with plugins specific command. """ Should retrun the parser with plugins specific command.
This is a basic example This is a basic example
""" """
@ -32,7 +32,7 @@ class PapersPlugin(object):
parser.add_argument('strings', nargs='*', help='the strings') parser.add_argument('strings', nargs='*', help='the strings')
return parser return parser
def command(self, config, ui, strings): def command(self, ui, strings):
"""This function will be called with argument defined in the parser above """This function will be called with argument defined in the parser above
This is a basic example This is a basic example
""" """
@ -47,7 +47,7 @@ class PapersPlugin(object):
raise RuntimeError("{} instance not created".format(cls.__name__)) raise RuntimeError("{} instance not created".format(cls.__name__))
def load_plugins(config, ui, names): def load_plugins(ui, names):
"""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
@ -69,7 +69,7 @@ def load_plugins(config, 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(config, ui) _instances[obj] = obj(ui)
except: except:
ui.warning('error loading plugin {}'.format(name)) ui.warning('error loading plugin {}'.format(name))

@ -3,7 +3,7 @@ import shutil
import subprocess import subprocess
from ... import repo from ... import repo
from ... import configs from ...configs import config
from ... import files from ... import files
from ...plugin import PapersPlugin from ...plugin import PapersPlugin
from ...commands.helpers import add_references_argument, parse_reference from ...commands.helpers import add_references_argument, parse_reference
@ -18,7 +18,7 @@ TEXNOTE_DIR = 'texnote'
class TexnotePlugin(PapersPlugin): class TexnotePlugin(PapersPlugin):
def parser(self, subparsers, config): def parser(self, subparsers):
parser = subparsers.add_parser(self.name, help="edit advance note in latex") parser = subparsers.add_parser(self.name, help="edit advance note in latex")
sub = parser.add_subparsers(title="valid texnote commands", dest="texcmd") sub = parser.add_subparsers(title="valid texnote commands", dest="texcmd")
p = sub.add_parser("remove", help="remove a reference") p = sub.add_parser("remove", help="remove a reference")
@ -29,11 +29,11 @@ class TexnotePlugin(PapersPlugin):
parser.add_argument('-v', '--view', action='store_true', help='open the paper in a pdf viewer', default=None) parser.add_argument('-v', '--view', action='store_true', help='open the paper in a pdf viewer', default=None)
return parser return parser
def command(self, config, ui, texcmd, reference, view): def command(self, ui, texcmd, reference, view):
if view is not None: if view is not None:
subprocess.Popen(['papers', 'open', reference]) subprocess.Popen(['papers', 'open', reference])
if texcmd == 'edit': if texcmd == 'edit':
open_texnote(config, ui, reference) open_texnote(ui, reference)
def toto(self): def toto(self):
print "toto" print "toto"
@ -47,7 +47,8 @@ class TexnotePlugin(PapersPlugin):
def remove(rmevent): def remove(rmevent):
texplug = TexnotePlugin.get_instance() texplug = TexnotePlugin.get_instance()
texplug.toto() texplug.toto()
rp = repo.Repository.from_directory(rmevent.config) # HACK : transfer repo via RemoveEvent, do not recreate one
rp = repo.Repository(config())
paper = rp.get_paper(parse_reference(rmevent.ui, rp, rmevent.citekey)) paper = rp.get_paper(parse_reference(rmevent.ui, rp, rmevent.citekey))
if 'texnote' in paper.metadata: if 'texnote' in paper.metadata:
try: try:
@ -59,8 +60,9 @@ def remove(rmevent):
files.save_meta(paper.metadata, metapath) files.save_meta(paper.metadata, metapath)
def open_texnote(config, ui, ref): def open_texnote(ui, ref):
rp = repo.Repository.from_directory(config) # HACK : transfer repo via arguments, do not recreate one
rp = repo.Repository(config())
paper = rp.get_paper(parse_reference(ui, rp, ref)) paper = rp.get_paper(parse_reference(ui, rp, ref))
#ugly to recode like for the doc field #ugly to recode like for the doc field
@ -83,12 +85,8 @@ def open_texnote(config, ui, ref):
autofill_texnote(texnote_path, paper.bibentry) autofill_texnote(texnote_path, paper.bibentry)
#open the file using the config editor #open the file using the config editor
if config.has_option(TEXNOTE_SECTION, 'edit-cmd'): edit_cmd = config(TEXTNOTE_SECTION).get('edit_cmd', config().edit_cmd)
#os.system(config.get(TEXNOTE_SECTION, 'edit-cmd') + ' ' + texnote_path + " &") subprocess.Popen([edit_cmd, texnote_path])
subprocess.Popen([config.get(TEXNOTE_SECTION, 'edit-cmd'), texnote_path])
else:
#os.system(config.get(configs.MAIN_SECTION, 'edit-cmd') + ' ' + texnote_path + " &")
subprocess.Popen([config.get(configs.MAIN_SECTION, 'edit-cmd'), texnote_path])
##### ugly replace by proper ##### ##### ugly replace by proper #####

@ -1,20 +1,21 @@
import os import os
import shutil import shutil
import glob import glob
import itertools
from . import files from . import files
from .paper import PaperInRepo, NoDocumentFile from .paper import PaperInRepo, NoDocumentFile
from . import configs from . import configs
ALPHABET = 'abcdefghijklmopqrstuvwxyz'
BASE_FILE = 'papers.yaml' BASE_FILE = 'papers.yaml'
BIB_DIR = 'bibdata' BIB_DIR = 'bibdata'
META_DIR = 'meta' META_DIR = 'meta'
DOC_DIR = 'doc' DOC_DIR = 'doc'
class CiteKeyAlreadyExists(Exception): class CiteKeyCollision(Exception):
pass pass
@ -24,24 +25,70 @@ class InvalidReference(Exception):
class Repository(object): class Repository(object):
def __init__(self, config=None): def __init__(self, config, load = True):
self.papersdir = None """Initialize the repository.
self.citekeys = []
if config is None:
config = configs.CONFIG
self.config = config
def has_paper(self, citekey): :param load: if load is True, load the repository from disk,
from path config.papers_dir.
"""
self.config = config
self.citekeys = []
if load:
self.load()
# @classmethod
# def from_directory(cls, config, papersdir=None):
# repo = cls(config)
# if papersdir is None:
# papersdir = config.papers_dir
# repo.papersdir = files.clean_path(papersdir)
# repo.load()
# return repo
def __contains__(self, citekey):
"""Allows to use 'if citekey in repo' pattern"""
return citekey in self.citekeys return citekey in self.citekeys
def get_paper(self, citekey): def __len__(self):
"""Load a paper by its citekey from disk, if necessary.""" return len(self.citekeys)
return PaperInRepo.load(
self, self.path_to_paper_file(citekey, 'bib'),
metapath=self.path_to_paper_file(citekey, 'meta')) # load, save repo
def _init_dirs(self, autodoc = True):
"""Create, if necessary, the repository directories.
Should only be called by load or save.
"""
self.bib_dir = files.clean_path(self.config.papers_dir, BIB_DIR)
self.meta_dir = files.clean_path(self.config.papers_dir, META_DIR)
if self.config.doc_dir == 'doc':
self.doc_dir = files.clean_path(self.config.papers_dir, DOC_DIR)
else:
self.doc_dir = files.clean_path(self.config.doc_dir)
self.cfg_path = files.clean_path(self.config.papers_dir, 'papers.yaml')
for d in [self.bib_dir, self.meta_dir, self.doc_dir]:
if not os.path.exists(d):
os.makedirs(d)
def load(self):
"""Load the repository, creating dirs if necessary"""
self._init_dirs()
repo_config = files.read_yamlfile(self.cfg_path)
self.citekeys = repo_config['citekeys']
def save(self):
"""Save the repo, creating dirs if necessary"""
self._init_dirs()
repo_cfg = {'citekeys': self.citekeys}
files.write_yamlfile(self.cfg_path, repo_cfg)
# reference
def citekey_from_ref(self, ref): def ref2citekey(self, ref):
"""Tries to get citekey from given ref. """Tries to get citekey from given reference.
Ref can be a citekey or a number. Ref can be a citekey or a number.
""" """
if ref in self.citekeys: if ref in self.citekeys:
@ -52,150 +99,112 @@ class Repository(object):
except (IndexError, ValueError): except (IndexError, ValueError):
raise(InvalidReference) raise(InvalidReference)
# creating new papers
def add_paper(self, p): # papers
def all_papers(self):
for key in self.citekeys:
yield self.get_paper(key)
def get_paper(self, citekey):
"""Load a paper by its citekey from disk, if necessary."""
return PaperInRepo.load(self, self._bibfile(citekey),
self._metafile(citekey))
# add, remove papers
def add_paper(self, p, overwrite = False):
if p.citekey is None: # TODO also test if citekey is valid if p.citekey is None: # TODO also test if citekey is valid
raise(ValueError("Invalid citekey: %s." % p.citekey)) raise(ValueError("Invalid citekey: %s." % p.citekey))
elif self.has_paper(p.citekey): if not overwrite and p.citekey in self:
raise(ValueError("Citekey already exists in repository: %s" raise CiteKeyCollision('citekey {} already in use'.format(
% p.citekey)) p.citekey))
self.citekeys.append(p.citekey) self.citekeys.append(p.citekey)
# write paper files
self.save_paper(p) self.save_paper(p)
# update repository files
self.save() self.save()
# TODO change to logging system (17/12/2012) # TODO change to logging system (17/12/2012)
print "Added: %s" % p.citekey print('Added: {}'.format(p.citekey))
return p
def add_or_update(self, paper):
if not self.has_paper(paper.citekey): def rename_paper(self, paper, new_citekey, overwrite=False):
self.add_paper(paper) """Modify the citekey of a paper, and propagate changes to disk"""
else: if paper.citekey not in self:
self.save_paper(paper) raise ValueError(
'paper {} not in repository'.format(paper.citekey))
def update(self, paper, old_citekey=None, overwrite=False): if (not overwrite and paper.citekey != new_citekey
"""Updates a paper, eventually changing its citekey. and new_citekey in self):
The paper should be in repository. If the citekey changes, raise CiteKeyCollision('citekey {} already in use'.format(
the new citekey should be free except if the overwrite argument new_citekey))
is set to True.
""" self.remove_paper(paper.citekey, remove_doc = False)
if old_citekey is None: old_citekey = paper.citekey
old_citekey = paper.citekey paper.citekey = new_citekey
if old_citekey not in self.citekeys: self.add_paper(paper, overwrite = overwrite)
raise(ValueError, 'Paper not in repository. Add first') self._move_doc(old_citekey, paper)
else:
if paper.citekey == old_citekey: def _move_doc(self, old_citekey, paper):
self.save_paper(paper) """Fragile. Make more robust"""
else:
if self.has_paper(paper.citekey):
if not overwrite:
raise(CiteKeyAlreadyExists,
"There is already a paper with citekey: %s."
% paper.citekey)
else:
self.save_paper(paper)
else:
self.add_paper(paper)
# Eventually move document file
paper = PaperInRepo.from_paper(paper, self)
try:
path = self.find_document(old_citekey)
self.import_document(paper.citekey, path)
except NoDocumentFile:
pass
self.remove(old_citekey)
def remove(self, citekey):
paper = self.get_paper(citekey)
self.citekeys.remove(citekey)
self.save()
for f in ('bib', 'meta'):
os.remove(self.path_to_paper_file(citekey, f))
# Eventually remove associated document
try: try:
path = paper.get_document_path_in_repo() old_docfile = self.find_document(old_citekey)
os.remove(path) ext = os.path.splitext(old_docfile)[1]
new_docfile = os.path.join(self.doc_dir, paper.citekey + ext)
shutil.move(old_docfile, new_docfile)
paper.set_external_document(new_docfile)
except NoDocumentFile: except NoDocumentFile:
pass pass
def save_paper(self, paper): def _bibfile(self, citekey):
if not self.has_paper(paper.citekey): return os.path.join(self.bib_dir, citekey + '.bibyaml')
raise(ValueError('Paper not in repository, first add it.'))
paper.save_to_disc(self.path_to_paper_file(paper.citekey, 'bib'),
self.path_to_paper_file(paper.citekey, 'meta'))
def get_free_citekey(self, paper, citekey=None): def _metafile(self, citekey):
"""Create a unique citekey for the given paper. return os.path.join(self.meta_dir, citekey + '.meta')
"""
if citekey is None:
citekey = paper.generate_citekey()
num = []
while citekey + _to_suffix(num) in self.citekeys:
_str_incr(num)
return citekey + _to_suffix(num)
def base_file_path(self):
return os.path.join(self.papersdir, 'papers.yaml')
def size(self): def remove_paper(self, citekey, remove_doc = True):
return len(self.citekeys) paper = self.get_paper(citekey)
self.citekeys.remove(citekey)
os.remove(self._metafile(citekey))
os.remove(self._bibfile(citekey))
def save(self): # Eventually remove associated document
papers_config = {'citekeys': self.citekeys} if remove_doc:
files.write_yamlfile(self.base_file_path(), papers_config) try:
path = paper.get_document_path_in_repo()
os.remove(path)
except NoDocumentFile:
pass
def load(self):
papers_config = files.read_yamlfile(self.base_file_path())
self.citekeys = papers_config['citekeys']
def init(self, papersdir):
self.papersdir = papersdir
os.makedirs(os.path.join(self.papersdir, BIB_DIR))
os.makedirs(os.path.join(self.papersdir, META_DIR))
doc_dir = self.get_document_directory()
if not os.path.exists(doc_dir):
os.makedirs(doc_dir)
self.save() self.save()
def path_to_paper_file(self, citekey, file_): def save_paper(self, paper):
if file_ == 'bib': if not paper.citekey in self:
return os.path.join(self.papersdir, BIB_DIR, citekey + '.bibyaml') raise(ValueError('Paper not in repository, first add it.'))
elif file_ == 'meta': paper.save(self._bibfile(paper.citekey),
return os.path.join(self.papersdir, META_DIR, citekey + '.meta') self._metafile(paper.citekey))
else:
raise(ValueError("%s is not a valid paper file." % file_))
def get_document_directory(self): def generate_citekey(self, paper, citekey=None):
if self.config.has_option(configs.MAIN_SECTION, 'document-directory'): """Create a unique citekey for the given paper."""
doc_dir = self.config.get(configs.MAIN_SECTION, if citekey is None:
'document-directory') citekey = paper.generate_citekey()
else: for n in itertools.count():
doc_dir = os.path.join(self.papersdir, DOC_DIR) if not citekey + _base27(n) in self.citekeys:
return files.clean_path(doc_dir) return citekey + _base27(n)
def find_document(self, citekey): def find_document(self, citekey):
doc_dir = self.get_document_directory() found = glob.glob('{}/{}.*'.format(self.doc_dir, citekey))
found = glob.glob(doc_dir + "/%s.*" % citekey)
if found: if found:
return found[0] return found[0]
else: else:
raise NoDocumentFile raise NoDocumentFile
def all_papers(self):
for key in self.citekeys:
yield self.get_paper(key)
def import_document(self, citekey, doc_file): def import_document(self, citekey, doc_file):
if citekey not in self.citekeys: if citekey not in self.citekeys:
raise(ValueError, "Unknown citekey: %s." % citekey) raise(ValueError, "Unknown citekey: %s." % citekey)
else: else:
doc_path = self.get_document_directory() if not os.path.isfile(doc_file):
if not (os.path.exists(doc_path) and os.path.isdir(doc_path)): raise ValueError("No file {} found.".format(doc_file))
raise(NoDocumentFile,
"Document directory %s, does not exist." % doc_path)
ext = os.path.splitext(doc_file)[1] ext = os.path.splitext(doc_file)[1]
new_doc_file = os.path.join(doc_path, citekey + ext) new_doc_file = os.path.join(self.doc_dir, citekey + ext)
shutil.copy(doc_file, new_doc_file) shutil.copy(doc_file, new_doc_file)
def get_tags(self): def get_tags(self):
@ -204,35 +213,12 @@ class Repository(object):
tags = tags.union(p.tags) tags = tags.union(p.tags)
return tags return tags
@classmethod
def from_directory(cls, config, papersdir=None):
repo = cls(config=config)
if papersdir is None:
papersdir = config.get(configs.MAIN_SECTION, 'papers-directory')
repo.papersdir = files.clean_path(papersdir)
repo.load()
return repo
def _char_incr(c):
return chr(ord(c) + 1)
def _str_incr(l):
"""Increment a number in a list string representation.
Numbers are represented in base 26 with letters as digits.
"""
pos = 0
while pos < len(l):
if l[pos] == 'z':
l[pos] = 'a'
pos += 1
else:
l[pos] = _char_incr(l[pos])
return
l.append('a')
def _base27(n):
return _base27((n-1) // 26) + chr(97+((n-1)% 26)) if n else ''
def _to_suffix(l): def _base(num, b):
return ''.join(l[::-1]) q, r = divmod(num - 1, len(b))
return _base(q, b) + b[r] if num else ''

@ -3,8 +3,6 @@ import sys
from .beets_ui import _encoding, input_ from .beets_ui import _encoding, input_
from . import color from . import color
from . import configs
class UI: class UI:
"""UI class. Stores configuration parameters and system information. """UI class. Stores configuration parameters and system information.
@ -12,8 +10,7 @@ class UI:
def __init__(self, config): def __init__(self, config):
self.encoding = _encoding(config) self.encoding = _encoding(config)
color_enable = configs.get_boolean(config.get('papers', 'color')) color.setup(config.color)
color.setup(color_enable)
def print_(self, *strings): def print_(self, *strings):
"""Like print, but rather than raising an error when a character """Like print, but rather than raising an error when a character

@ -3,7 +3,7 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
setup(name='papers', setup(name='papers',
version='1', version='3',
author='Fabien Benureau, Olivier Mangin, Jonathan Grizou', author='Fabien Benureau, Olivier Mangin, Jonathan Grizou',
author_email='fabien.benureau+inria@gmail.com', author_email='fabien.benureau+inria@gmail.com',
url='', url='',

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
import unittest
import testenv
from papers import configs
from papers.configs import config
from papers.p3 import configparser
class TestConfig(unittest.TestCase):
def test_create_config(self):
a = configs.Config()
a.as_global()
self.assertEqual(a, config())
def test_config_content(self):
a = configs.Config()
a.as_global()
self.assertEqual(config().papers_dir, configs.DFT_CONFIG['papers_dir'])
self.assertEqual(config().color, configs.str2bool(configs.DFT_CONFIG['color']))
def test_set(self):
a = configs.Config()
a.as_global()
config().color = 'no'
self.assertEqual(config().color, False)
self.assertEqual(config('papers').color, False)
# booleans type for new variables are memorized, but not saved.
config().bla = True
self.assertEqual(config().bla, True)
self.assertEqual(config('papers').bla, True)
with self.assertRaises(configparser.NoOptionError):
config()._cfg.get(configs.MAIN_SECTION, '_section')
def test_reload(self):
default_color = configs.DFT_CONFIG['color']
a = configs.Config()
a.as_global()
a.color = False
a.bla = 'foo'
config.color = not configs.str2bool(default_color)
self.assertEqual(config().color, not configs.str2bool(default_color))
b = configs.Config()
b.as_global()
self.assertEqual(b, config())
self.assertEqual(config().color, configs.str2bool(default_color))
def test_exception(self):
a = configs.Config()
a.as_global()
with self.assertRaises(configparser.NoOptionError):
config().color2
self.assertEqual(config().get('color2', default = 'blue'), 'blue')
with self.assertRaises(configparser.NoSectionError):
config(section = 'bla3').color
self.assertEqual(config(section = 'bla3').get('color', default = 'green'), 'green')
self.assertEqual(config(section = 'bla3').get('color', default = config().color), True)
def test_keywords(self):
a = configs.Config(papers_dir = '/blabla')
self.assertEqual(a.papers_dir, '/blabla')

@ -1,5 +1,6 @@
from unittest import TestCase from unittest import TestCase
import testenv
from papers.events import Event from papers.events import Event

@ -7,6 +7,7 @@ import shutil
import yaml import yaml
from pybtex.database import Person from pybtex.database import Person
import testenv
import fixtures import fixtures
from papers.paper import Paper from papers.paper import Paper
@ -73,25 +74,25 @@ class TestSaveLoad(unittest.TestCase):
def test_save_fails_with_no_citekey(self): def test_save_fails_with_no_citekey(self):
p = Paper() p = Paper()
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
p.save_to_disc(self.dest_bibfile, self.dest_metafile) p.save(self.dest_bibfile, self.dest_metafile)
def test_save_creates_bib(self): def test_save_creates_bib(self):
fixtures.turing1950.save_to_disc(self.dest_bibfile, self.dest_metafile) fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile)
self.assertTrue(os.path.exists(self.dest_bibfile)) self.assertTrue(os.path.exists(self.dest_bibfile))
def test_save_creates_meta(self): def test_save_creates_meta(self):
fixtures.turing1950.save_to_disc(self.dest_bibfile, self.dest_metafile) fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile)
self.assertTrue(os.path.exists(self.dest_metafile)) self.assertTrue(os.path.exists(self.dest_metafile))
def test_save_right_bib(self): def test_save_right_bib(self):
fixtures.turing1950.save_to_disc(self.dest_bibfile, self.dest_metafile) fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile)
with open(self.dest_bibfile, 'r') as f: with open(self.dest_bibfile, 'r') as f:
written = yaml.load(f) written = yaml.load(f)
ok = yaml.load(BIB) ok = yaml.load(BIB)
self.assertEqual(written, ok) self.assertEqual(written, ok)
def test_save_right_meta(self): def test_save_right_meta(self):
fixtures.turing1950.save_to_disc(self.dest_bibfile, self.dest_metafile) fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile)
with open(self.dest_metafile, 'r') as f: with open(self.dest_metafile, 'r') as f:
written = yaml.load(f) written = yaml.load(f)
ok = yaml.load(META) ok = yaml.load(META)

@ -3,31 +3,27 @@ import tempfile
import shutil import shutil
import os import os
import testenv
import fixtures import fixtures
from papers.repo import (Repository, _str_incr, _to_suffix, BIB_DIR, META_DIR, from papers.repo import (Repository, _base27, BIB_DIR, META_DIR,
CiteKeyAlreadyExists) CiteKeyCollision)
from papers.paper import PaperInRepo from papers.paper import PaperInRepo
from papers import configs, files
class TestCitekeyGeneration(unittest.TestCase): class TestCitekeyGeneration(unittest.TestCase):
def test_string_increment(self): def test_string_increment(self):
l = [] self.assertEqual(_base27(0), '')
self.assertEqual(_to_suffix(l), '') for i in range(26):
_str_incr(l) self.assertEqual(_base27(i+1), chr(97+i))
self.assertEqual(_to_suffix(l), 'a') self.assertEqual(_base27(26+i+1), 'a' + chr(97+i))
_str_incr(l)
self.assertEqual(_to_suffix(l), 'b')
l = ['z']
_str_incr(l)
self.assertEqual(_to_suffix(l), 'aa')
def test_generated_key_is_unique(self): def test_generated_key_is_unique(self):
repo = Repository() repo = Repository(configs.Config(), load = False)
repo.citekeys = ['Turing1950', 'Doe2003'] repo.citekeys = ['Turing1950', 'Doe2003']
c = repo.get_free_citekey(fixtures.turing1950) c = repo.generate_citekey(fixtures.turing1950)
repo.citekeys.append('Turing1950a') repo.citekeys.append('Turing1950a')
c = repo.get_free_citekey(fixtures.turing1950) c = repo.generate_citekey(fixtures.turing1950)
self.assertEqual(c, 'Turing1950b') self.assertEqual(c, 'Turing1950b')
@ -35,8 +31,8 @@ class TestRepo(unittest.TestCase):
def setUp(self): def setUp(self):
self.tmpdir = tempfile.mkdtemp() self.tmpdir = tempfile.mkdtemp()
self.repo = Repository() self.repo = Repository(configs.Config(papers_dir = self.tmpdir), load = False)
self.repo.init(self.tmpdir) self.repo.save()
self.repo.add_paper(fixtures.turing1950) self.repo.add_paper(fixtures.turing1950)
def tearDown(self): def tearDown(self):
@ -46,15 +42,19 @@ class TestRepo(unittest.TestCase):
class TestAddPaper(TestRepo): class TestAddPaper(TestRepo):
def test_raises_value_error_on_existing_key(self): def test_raises_value_error_on_existing_key(self):
with self.assertRaises(ValueError): with self.assertRaises(CiteKeyCollision):
self.repo.add_paper(fixtures.turing1950) self.repo.add_paper(fixtures.turing1950)
def test_saves_bib(self): def test_saves_bib(self):
self.assertTrue(os.path.exists(os.path.join(self.tmpdir, BIB_DIR, self.assertEqual(files.clean_path(self.tmpdir, BIB_DIR),
files.clean_path(self.repo.bib_dir))
self.assertTrue(os.path.exists(os.path.join(self.repo.bib_dir,
'Turing1950.bibyaml'))) 'Turing1950.bibyaml')))
def test_saves_meta(self): def test_saves_meta(self):
self.assertTrue(os.path.exists(os.path.join(self.tmpdir, META_DIR, self.assertEqual(files.clean_path(self.tmpdir, META_DIR),
files.clean_path(self.repo.meta_dir))
self.assertTrue(os.path.exists(os.path.join(self.repo.meta_dir,
'Turing1950.meta'))) 'Turing1950.meta')))
@ -62,34 +62,37 @@ class TestUpdatePaper(TestRepo):
def test_raises_value_error_on_unknown_paper(self): def test_raises_value_error_on_unknown_paper(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.repo.update(fixtures.doe2013) self.repo.save_paper(fixtures.doe2013)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.repo.update(fixtures.doe2013, old_citekey='zou') self.repo.rename_paper(fixtures.doe2013, 'zou')
def test_error_on_existing_destination(self): def test_error_on_existing_destination(self):
self.repo.add_paper(fixtures.doe2013) self.repo.add_paper(fixtures.doe2013)
with self.assertRaises(CiteKeyAlreadyExists): with self.assertRaises(CiteKeyCollision):
self.repo.update(fixtures.turing1950, old_citekey='Doe2013') self.repo.rename_paper(fixtures.turing1950, 'Doe2013')
def test_updates_same_key(self): def test_updates_same_key(self):
new = self.repo.get_paper('Turing1950') new = self.repo.get_paper('Turing1950')
new.bibentry.fields['journal'] = u'Mind' new.bibentry.fields['journal'] = u'Mind'
self.repo.update(new) self.repo.save_paper(new)
self.assertEqual(new, self.repo.get_paper('Turing1950')) self.assertEqual(new, self.repo.get_paper('Turing1950'))
def test_updates_same_key_with_old_arg(self): def test_updates_same_key_with_old_arg(self):
new = self.repo.get_paper('Turing1950') new = self.repo.get_paper('Turing1950')
new.bibentry.fields['journal'] = u'Mind' new.bibentry.fields['journal'] = u'Mind'
self.repo.update(new, old_citekey='Turing1950') self.repo.rename_paper(new, 'Turing1950')
self.assertEqual(new, self.repo.get_paper('Turing1950')) self.assertEqual(new, self.repo.get_paper('Turing1950'))
def test_update_new_key_removes_old(self): def test_update_new_key_removes_old(self):
self.repo.update(fixtures.doe2013, old_citekey='Turing1950') self.repo.add_paper(fixtures.doe2013)
self.assertFalse(self.repo.has_paper('Turing1950')) self.repo.rename_paper(fixtures.doe2013, 'JohnDoe2003')
self.assertFalse('Doe2003' in self.repo)
def test_update_new_key_updates(self): def test_update_new_key_updates(self):
self.repo.update(fixtures.doe2013, old_citekey='Turing1950') # self.repo.rename(fixtures.doe2013, old_citekey='Turing1950')
self.assertTrue(self.repo.has_paper('Doe2013')) fixtures.doe2013.citekey = 'Doe2013'
self.repo.add_paper(fixtures.doe2013)
self.assertTrue('Doe2013' in self.repo)
self.assertEqual(self.repo.get_paper('Doe2013'), self.assertEqual(self.repo.get_paper('Doe2013'),
PaperInRepo.from_paper(fixtures.doe2013, self.repo)) PaperInRepo.from_paper(fixtures.doe2013, self.repo))
@ -97,8 +100,8 @@ class TestUpdatePaper(TestRepo):
self.repo.import_document('Turing1950', self.repo.import_document('Turing1950',
os.path.join(os.path.dirname(__file__), os.path.join(os.path.dirname(__file__),
'data/pagerank.pdf')) 'data/pagerank.pdf'))
self.repo.update(fixtures.doe2013, old_citekey='Turing1950') self.repo.rename_paper(self.repo.get_paper('Turing1950'), 'Doe2003')
self.assertFalse(os.path.exists(os.path.join( self.assertFalse(os.path.exists(os.path.join(
self.repo.get_document_directory(), 'Turing1950.pdf'))) self.repo.doc_dir, 'Turing1950.pdf')))
self.assertTrue(os.path.exists(os.path.join( self.assertTrue(os.path.exists(os.path.join(
self.repo.get_document_directory(), 'Doe2013.pdf'))) self.repo.doc_dir, 'Doe2003.pdf')))

@ -1,10 +1,11 @@
import sys, os import sys, os, shutil, glob
import unittest import unittest
import pkgutil import pkgutil
import testenv import testenv
import fake_filesystem import fake_filesystem
import fake_filesystem_shutil import fake_filesystem_shutil
import fake_filesystem_glob
from papers import papers_cmd from papers import papers_cmd
from papers import color from papers import color
@ -12,23 +13,27 @@ from papers.p3 import io
real_os = os real_os = os
real_open = open real_open = open
real_shutil = shutil
real_glob = glob
fake_os, fake_open, fake_shutil = None, None, None fake_os, fake_open, fake_shutil, fake_glob = None, None, None, None
def _create_fake_fs(): def _create_fake_fs():
global fake_os, fake_open, fake_shutil global fake_os, fake_open, fake_shutil, fake_glob
fake_fs = fake_filesystem.FakeFilesystem() fake_fs = fake_filesystem.FakeFilesystem()
fake_os = fake_filesystem.FakeOsModule(fake_fs) fake_os = fake_filesystem.FakeOsModule(fake_fs)
fake_open = fake_filesystem.FakeFileOpen(fake_fs) fake_open = fake_filesystem.FakeFileOpen(fake_fs)
fake_shutil = fake_filesystem_shutil.FakeShutilModule(fake_fs) fake_shutil = fake_filesystem_shutil.FakeShutilModule(fake_fs)
fake_glob = fake_filesystem_glob.FakeGlobModule(fake_fs)
fake_fs.CreateDirectory(fake_os.path.expanduser('~')) fake_fs.CreateDirectory(fake_os.path.expanduser('~'))
__builtins__['open'] = fake_open __builtins__['open'] = fake_open
__builtins__['file'] = fake_open __builtins__['file'] = fake_open
sys.modules['os'] = fake_os sys.modules['os'] = fake_os
sys.modules['shutil'] = fake_shutil sys.modules['shutil'] = fake_shutil
sys.modules['glob'] = fake_glob
import papers import papers
for importer, modname, ispkg in pkgutil.walk_packages( for importer, modname, ispkg in pkgutil.walk_packages(
@ -122,7 +127,7 @@ class TestUsecase(unittest.TestCase):
def test_first(self): def test_first(self):
correct = ['Initializing papers in /paper_first/.\n', correct = ['Initializing papers in /paper_first.\n',
'Added: Page99\n', 'Added: Page99\n',
'0: [Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n', '0: [Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n',
'', '',

Loading…
Cancel
Save