make utf8 citekeys possible in python 2.7. closes #28
This involved many changes, some side effects of the change include: - remove of all `u"abc"` forms, in favor of `from __future__ import unicode_literals`. Their usage was inconsistent anyway, leading to problems when mixing with unicode content. - improve the tests, to allow printing for usecase even when crashing. Should make future test easier. This is done with a rather hacky `StdIO` class in `p3`, but it works. - for some reason, the skipped test for Python 2 seems to work now. While the previous point might seem related, it is not clear that this is actually the case.
This commit is contained in:
parent
38133fc053
commit
dc4e118c3c
@ -25,14 +25,14 @@ def str2citekey(s):
|
||||
|
||||
def check_citekey(citekey):
|
||||
if citekey is None or not citekey.strip():
|
||||
raise ValueError(u"Empty citekeys are not valid")
|
||||
raise ValueError("Empty citekeys are not valid")
|
||||
|
||||
|
||||
def verify_bibdata(bibdata):
|
||||
if bibdata is None or len(bibdata) == 0:
|
||||
raise ValueError(u"no valid bibdata")
|
||||
raise ValueError("no valid bibdata")
|
||||
if len(bibdata) > 1:
|
||||
raise ValueError(u"ambiguous: multiple entries in the bibdata.")
|
||||
raise ValueError("ambiguous: multiple entries in the bibdata.")
|
||||
|
||||
|
||||
def get_entry(bibdata):
|
||||
@ -64,12 +64,12 @@ def generate_citekey(bibdata):
|
||||
first_author = entry[author_key][0]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
u"No author or editor defined: cannot generate a citekey.")
|
||||
"No author or editor defined: cannot generate a citekey.")
|
||||
try:
|
||||
year = entry['year']
|
||||
except KeyError:
|
||||
year = ''
|
||||
citekey = u'{}{}'.format(u''.join(author_last(first_author)), year)
|
||||
citekey = '{}{}'.format(''.join(author_last(first_author)), year)
|
||||
|
||||
return str2citekey(citekey)
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
"""
|
||||
Code to handle colored text
|
||||
"""
|
||||
|
||||
"""
|
||||
Here is a little explanation about bash color code, useful to understand
|
||||
the code below. See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
for a complete referece.
|
||||
@ -25,6 +23,7 @@ by the bright version of the font; some terminals allow the user to decide that.
|
||||
display colors, with 0 <= c < 8 corresponding to the 8 above colors, and
|
||||
8 <= c < 16 their bright version.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import re
|
||||
@ -32,15 +31,15 @@ import os
|
||||
import subprocess
|
||||
|
||||
|
||||
COLOR_LIST = {u'black': '0', u'red': '1', u'green': '2', u'yellow': '3', u'blue': '4',
|
||||
u'magenta': '5', u'cyan': '6', u'grey': '7',
|
||||
u'brightblack': '8', u'brightred': '9', u'brightgreen': '10',
|
||||
u'brightyellow': '11', u'brightblue': '12', u'brightmagenta': '13',
|
||||
u'brightcyan': '14', u'brightgrey': '15',
|
||||
u'darkgrey': '8', # == brightblack
|
||||
u'gray': '7', u'darkgray': '8', u'brightgray': '15', # gray/grey spelling
|
||||
u'purple': '5', # for compatibility reasons
|
||||
u'white': '15' # == brightgrey
|
||||
COLOR_LIST = {'black': '0', 'red': '1', 'green': '2', 'yellow': '3', 'blue': '4',
|
||||
'magenta': '5', 'cyan': '6', 'grey': '7',
|
||||
'brightblack': '8', 'brightred': '9', 'brightgreen': '10',
|
||||
'brightyellow': '11', 'brightblue': '12', 'brightmagenta': '13',
|
||||
'brightcyan': '14', 'brightgrey': '15',
|
||||
'darkgrey': '8', # == brightblack
|
||||
'gray': '7', 'darkgray': '8', 'brightgray': '15', # gray/grey spelling
|
||||
'purple': '5', # for compatibility reasons
|
||||
'white': '15' # == brightgrey
|
||||
}
|
||||
for c in range(256):
|
||||
COLOR_LIST[str(c)] = str(c)
|
||||
@ -74,43 +73,43 @@ def generate_colors(stream, color=True, bold=True, italic=True, force_colors=Fal
|
||||
normal colors.
|
||||
:param italic: generate italic colors
|
||||
"""
|
||||
colors = {u'bold': u'', u'italic': u'', u'end': u'', u'': u''}
|
||||
colors = {'bold': '', 'italic': '', 'end': '', '': ''}
|
||||
for name, code in COLOR_LIST.items():
|
||||
colors[name] = u''
|
||||
colors[u'b' +name] = u''
|
||||
colors[u'i' +name] = u''
|
||||
colors[u'bi'+name] = u''
|
||||
colors[name] = ''
|
||||
colors['b' +name] = ''
|
||||
colors['i' +name] = ''
|
||||
colors['bi'+name] = ''
|
||||
|
||||
color_support = _color_supported(stream, force=force_colors) >= 8
|
||||
|
||||
if (color or bold or italic) and color_support:
|
||||
bold_flag, italic_flag = '', ''
|
||||
if bold:
|
||||
colors['bold'] = u'\033[1m'
|
||||
colors['bold'] = '\033[1m'
|
||||
bold_flag = '1;'
|
||||
if italic:
|
||||
colors['italic'] = u'\033[3m'
|
||||
colors['italic'] = '\033[3m'
|
||||
italic_flag = '3;'
|
||||
if bold and italic:
|
||||
colors['bolditalic'] = u'\033[1;3m'
|
||||
colors['bolditalic'] = '\033[1;3m'
|
||||
|
||||
for name, code in COLOR_LIST.items():
|
||||
if color:
|
||||
colors[name] = u'\033[38;5;{}m'.format(code)
|
||||
colors[u'b'+name] = u'\033[{}38;5;{}m'.format(bold_flag, code)
|
||||
colors[u'i'+name] = u'\033[{}38;5;{}m'.format(italic_flag, code)
|
||||
colors[u'bi'+name] = u'\033[{}38;5;{}m'.format(bold_flag, italic_flag, code)
|
||||
colors[name] = '\033[38;5;{}m'.format(code)
|
||||
colors['b'+name] = '\033[{}38;5;{}m'.format(bold_flag, code)
|
||||
colors['i'+name] = '\033[{}38;5;{}m'.format(italic_flag, code)
|
||||
colors['bi'+name] = '\033[{}38;5;{}m'.format(bold_flag, italic_flag, code)
|
||||
|
||||
else:
|
||||
if bold:
|
||||
colors.update({u'b'+name: u'\033[1m' for i, name in enumerate(COLOR_LIST)})
|
||||
colors.update({'b'+name: '\033[1m' for i, name in enumerate(COLOR_LIST)})
|
||||
if italic:
|
||||
colors.update({u'i'+name: u'\033[3m' for i, name in enumerate(COLOR_LIST)})
|
||||
colors.update({'i'+name: '\033[3m' for i, name in enumerate(COLOR_LIST)})
|
||||
if bold or italic:
|
||||
colors.update({u'bi'+name: u'\033[{}{}m'.format(bold_flag, italic_flag) for i, name in enumerate(COLOR_LIST)})
|
||||
colors.update({'bi'+name: '\033[{}{}m'.format(bold_flag, italic_flag) for i, name in enumerate(COLOR_LIST)})
|
||||
|
||||
if color or bold or italic:
|
||||
colors[u'end'] = u'\033[0m'
|
||||
colors['end'] = '\033[0m'
|
||||
|
||||
return colors
|
||||
|
||||
@ -121,11 +120,11 @@ COLORS_ERR = generate_colors(sys.stderr, color=False, bold=False, italic=False)
|
||||
|
||||
def dye_out(s, color='end'):
|
||||
"""Color a string for output on stdout"""
|
||||
return u'{}{}{}'.format(COLORS_OUT[color], s, COLORS_OUT['end'])
|
||||
return '{}{}{}'.format(COLORS_OUT[color], s, COLORS_OUT['end'])
|
||||
|
||||
def dye_err(s, color='end'):
|
||||
"""Color a string for output on stderr"""
|
||||
return u'{}{}{}'.format(COLORS_ERR[color], s, COLORS_OUT['end'])
|
||||
return '{}{}{}'.format(COLORS_ERR[color], s, COLORS_OUT['end'])
|
||||
|
||||
|
||||
def setup(conf, force_colors=False):
|
||||
|
@ -1,5 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import argparse
|
||||
from ..uis import get_ui
|
||||
from .. import p3
|
||||
from .. import bibstruct
|
||||
from .. import content
|
||||
from .. import repo
|
||||
@ -29,7 +32,7 @@ def parser(subparsers, conf):
|
||||
default=None
|
||||
).completer = CommaSeparatedTagsCompletion(conf)
|
||||
parser.add_argument('-k', '--citekey', help='citekey associated with the paper;\nif not provided, one will be generated automatically.',
|
||||
default=None)
|
||||
default=None, type=p3.to_utf8)
|
||||
parser.add_argument('-L', '--link', action='store_false', dest='copy', default=True,
|
||||
help="don't copy document files, just create a link.")
|
||||
parser.add_argument('-M', '--move', action='store_true', dest='move', default=False,
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .. import uis
|
||||
from .. import config
|
||||
from .. import content
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ..paper import Paper
|
||||
from .. import repo
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import argparse
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import datetime
|
||||
|
||||
@ -78,10 +80,10 @@ def command(conf, args):
|
||||
for k in keys:
|
||||
p = papers[k]
|
||||
if isinstance(p, Exception):
|
||||
ui.error(u'Could not load entry for citekey {}.'.format(k))
|
||||
ui.error('Could not load entry for citekey {}.'.format(k))
|
||||
else:
|
||||
rp.push_paper(p, overwrite=args.overwrite)
|
||||
ui.info(u'{} imported.'.format(color.dye_out(p.citekey, 'citekey')))
|
||||
ui.info('{} imported.'.format(color.dye_out(p.citekey, 'citekey')))
|
||||
docfile = bibstruct.extract_docfile(p.bibdata)
|
||||
if docfile is None:
|
||||
ui.warning("No file for {}.".format(p.citekey))
|
||||
|
@ -1,4 +1,5 @@
|
||||
# init command
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from .. import repo
|
||||
|
@ -7,7 +7,7 @@ from ..completion import CiteKeyCompletion
|
||||
def parser(subparsers, conf):
|
||||
parser = subparsers.add_parser('note',
|
||||
help='edit the note attached to a paper')
|
||||
parser.add_argument('citekey', help='citekey of the paper'
|
||||
parser.add_argument('citekey', help='citekey of the paper',
|
||||
).completer = CiteKeyCompletion(conf)
|
||||
return parser
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .. import repo
|
||||
from .. import color
|
||||
from ..uis import get_ui
|
||||
from ..utils import resolve_citekey_list
|
||||
from ..p3 import ustr
|
||||
from ..p3 import ustr, to_utf8
|
||||
from ..completion import CiteKeyCompletion
|
||||
|
||||
|
||||
@ -10,8 +12,8 @@ def parser(subparsers, conf):
|
||||
parser = subparsers.add_parser('remove', help='removes a publication')
|
||||
parser.add_argument('-f', '--force', action='store_true', default=None,
|
||||
help="does not prompt for confirmation.")
|
||||
parser.add_argument('citekeys', nargs='+',
|
||||
help="one or several citekeys"
|
||||
parser.add_argument('citekeys', nargs='+', type=to_utf8,
|
||||
help="one or several citekeys",
|
||||
).completer = CiteKeyCompletion(conf)
|
||||
return parser
|
||||
|
||||
@ -21,6 +23,7 @@ def command(conf, args):
|
||||
ui = get_ui()
|
||||
force = args.force
|
||||
rp = repo.Repository(conf)
|
||||
print(type(args.citekeys[0]), args.citekeys[0])
|
||||
|
||||
keys = resolve_citekey_list(repo=rp, citekeys=args.citekeys, ui=ui, exit_on_fail=True)
|
||||
|
||||
@ -37,10 +40,12 @@ def command(conf, args):
|
||||
except Exception as e:
|
||||
ui.error(ustr(e))
|
||||
failed = True
|
||||
ui.message('The publication(s) [{}] were removed'.format(
|
||||
', '.join([color.dye_out(c, 'citekey') for c in keys])))
|
||||
if failed:
|
||||
ui.exit() # Exit with nonzero error code
|
||||
else:
|
||||
ui.message('The publication(s) [{}] were removed'.format(
|
||||
', '.join([color.dye_out(c, 'citekey') for c in keys])))
|
||||
|
||||
# FIXME: print should check that removal proceeded well.
|
||||
else:
|
||||
ui.message('The publication(s) [{}] were {} removed'.format(
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ..uis import get_ui
|
||||
from .. import color
|
||||
from .. import repo
|
||||
|
@ -16,6 +16,7 @@ The different use cases are :
|
||||
7. > pubs tag -war+math+romance
|
||||
display all papers with the tag 'math', 'romance' but not 'war'
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import webbrowser
|
||||
|
||||
from .. import p3
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
@ -28,7 +30,7 @@ class UnableToDecodeTextFile(Exception):
|
||||
def _check_system_path_exists(path, fail=True):
|
||||
answer = os.path.exists(path)
|
||||
if not answer and fail:
|
||||
raise IOError(u'File does not exist: {}'.format(path))
|
||||
raise IOError('File does not exist: {}'.format(path))
|
||||
else:
|
||||
return answer
|
||||
|
||||
@ -37,7 +39,7 @@ def _check_system_path_is(nature, path, fail=True):
|
||||
check_fun = getattr(os.path, nature)
|
||||
answer = check_fun(path)
|
||||
if not answer and fail:
|
||||
raise IOError(u'{} is not a {}.'.format(path, nature))
|
||||
raise IOError('{} is not a {}.'.format(path, nature))
|
||||
else:
|
||||
return answer
|
||||
|
||||
@ -56,13 +58,13 @@ def _open(path, mode):
|
||||
def check_file(path, fail=True):
|
||||
syspath = system_path(path)
|
||||
return (_check_system_path_exists(syspath, fail=fail) and
|
||||
_check_system_path_is(u'isfile', syspath, fail=fail))
|
||||
_check_system_path_is('isfile', syspath, fail=fail))
|
||||
|
||||
|
||||
def check_directory(path, fail=True):
|
||||
syspath = system_path(path)
|
||||
return (_check_system_path_exists(syspath, fail=fail) and
|
||||
_check_system_path_is(u'isdir', syspath, fail=fail))
|
||||
_check_system_path_is('isdir', syspath, fail=fail))
|
||||
|
||||
|
||||
def read_text_file(filepath, fail=True):
|
||||
@ -112,23 +114,23 @@ def write_file(filepath, data, mode='w'):
|
||||
|
||||
def content_type(path):
|
||||
parsed = urlparse(path)
|
||||
if parsed.scheme == u'http':
|
||||
return u'url'
|
||||
if parsed.scheme == 'http':
|
||||
return 'url'
|
||||
else:
|
||||
return u'file'
|
||||
return 'file'
|
||||
|
||||
|
||||
def url_exists(url):
|
||||
parsed = urlparse(url)
|
||||
conn = HTTPConnection(parsed.netloc)
|
||||
conn.request(u'HEAD', parsed.path)
|
||||
conn.request('HEAD', parsed.path)
|
||||
response = conn.getresponse()
|
||||
conn.close()
|
||||
return response.status == 200
|
||||
|
||||
|
||||
def check_content(path):
|
||||
if content_type(path) == u'url':
|
||||
if content_type(path) == 'url':
|
||||
return url_exists(path)
|
||||
else:
|
||||
return check_file(path)
|
||||
@ -136,7 +138,7 @@ def check_content(path):
|
||||
|
||||
def _get_byte_url_content(path, ui=None):
|
||||
if ui is not None:
|
||||
ui.message(u'dowloading {}'.format(path))
|
||||
ui.message('dowloading {}'.format(path))
|
||||
response = urlopen(path)
|
||||
return response.read()
|
||||
|
||||
@ -151,7 +153,7 @@ def _dump_byte_url_content(source, target):
|
||||
|
||||
def get_content(path, ui=None):
|
||||
"""Will be useful when we need to get content from url"""
|
||||
if content_type(path) == u'url':
|
||||
if content_type(path) == 'url':
|
||||
return _get_byte_url_content(path, ui=ui).decode(encoding='utf-8')
|
||||
else:
|
||||
return read_text_file(path)
|
||||
@ -163,19 +165,19 @@ def move_content(source, target, overwrite=False):
|
||||
if source == target:
|
||||
return
|
||||
if not overwrite and os.path.exists(target):
|
||||
raise IOError(u'target file exists')
|
||||
raise IOError('target file exists')
|
||||
shutil.move(source, target)
|
||||
|
||||
|
||||
def copy_content(source, target, overwrite=False):
|
||||
source_is_url = content_type(source) == u'url'
|
||||
source_is_url = content_type(source) == 'url'
|
||||
if not source_is_url:
|
||||
source = system_path(source)
|
||||
target = system_path(target)
|
||||
if source == target:
|
||||
return
|
||||
if not overwrite and os.path.exists(target):
|
||||
raise IOError(u'{} file exists.'.format(target))
|
||||
raise IOError('{} file exists.'.format(target))
|
||||
if source_is_url:
|
||||
_dump_byte_url_content(source, target)
|
||||
else:
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from . import filebroker
|
||||
from . import endecoder
|
||||
from .p3 import pickle
|
||||
@ -76,7 +78,7 @@ class DataBroker(object):
|
||||
def verify(self, bibdata_raw):
|
||||
"""Will return None if bibdata_raw can't be decoded"""
|
||||
try:
|
||||
if bibdata_raw.startswith(u'\ufeff'):
|
||||
if bibdata_raw.startswith('\ufeff'):
|
||||
# remove BOM, because bibtexparser does not support it.
|
||||
bibdata_raw = bibdata_raw[1:]
|
||||
return self.endecoder.decode_bibdata(bibdata_raw)
|
||||
|
@ -1,5 +1,4 @@
|
||||
from __future__ import (print_function, absolute_import, division,
|
||||
unicode_literals)
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import copy
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
from .p3 import urlparse
|
||||
from .p3 import urlparse, u_maybe
|
||||
|
||||
from .content import (check_file, check_directory, read_text_file, write_file,
|
||||
system_path, check_content, copy_content)
|
||||
@ -18,7 +18,7 @@ def filter_filename(filename, ext):
|
||||
"""
|
||||
pattern = '.*\{}$'.format(ext)
|
||||
if re.match(pattern, filename) is not None:
|
||||
return filename[:-len(ext)]
|
||||
return u_maybe(filename[:-len(ext)])
|
||||
|
||||
|
||||
class FileBroker(object):
|
||||
|
72
pubs/p3.py
72
pubs/p3.py
@ -1,5 +1,10 @@
|
||||
import io
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from six import b, u
|
||||
|
||||
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
import cPickle as pickle
|
||||
@ -21,12 +26,46 @@ if sys.version_info[0] == 2:
|
||||
from urllib2 import urlopen
|
||||
from httplib import HTTPConnection
|
||||
file = None
|
||||
_fake_stdio = io.BytesIO # Only for tests to capture std{out,err}
|
||||
|
||||
def u_maybe(s):
|
||||
"""Convert to unicode, but only if necessary"""
|
||||
if isinstance(s, str):
|
||||
s = s.decode('utf-8')
|
||||
return s
|
||||
|
||||
class StdIO(io.BytesIO):
|
||||
"""Enable printing the streams received by a BytesIO instance"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.additional_out = kwargs.pop('additional_out')
|
||||
super(StdIO, self).__init__(*args, **kwargs)
|
||||
|
||||
def write(self, s):
|
||||
if self.additional_out is not None:
|
||||
self.additional_out.write(s)
|
||||
|
||||
super(StdIO, self).write(b(s))
|
||||
|
||||
_fake_stdio = StdIO # Only for tests to capture std{out,err}
|
||||
|
||||
def _get_fake_stdio_ucontent(stdio):
|
||||
ustdio = io.TextIOWrapper(stdio)
|
||||
ustdio.seek(0)
|
||||
return ustdio.read()
|
||||
|
||||
# ustdio = io.TextIOWrapper(stdio)
|
||||
stdio.seek(0)
|
||||
return stdio.read()
|
||||
|
||||
def to_utf8(s):
|
||||
return b(s)
|
||||
|
||||
# for details, seehttp://bugs.python.org/issue9779
|
||||
class ArgumentParser(argparse.ArgumentParser):
|
||||
def _print_message(self, message, file=None):
|
||||
"""Fixes the lack of a buffer interface in unicode object """
|
||||
if message:
|
||||
if file is None:
|
||||
file = _sys.stderr
|
||||
file.write(message.encode('utf-8'))
|
||||
|
||||
|
||||
|
||||
else:
|
||||
ustr = str
|
||||
@ -43,16 +82,37 @@ else:
|
||||
def _get_raw_stderr():
|
||||
return sys.stderr.buffer
|
||||
|
||||
def _fake_stdio():
|
||||
return io.TextIOWrapper(io.BytesIO()) # Only for tests to capture std{out,err}
|
||||
def u_maybe(s):
|
||||
return s
|
||||
|
||||
class StdIO(io.BytesIO):
|
||||
"""Enable printing the streams received by a BytesIO instance"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.additional_out = kwargs.pop('additional_out')
|
||||
super(StdIO, self).__init__(*args, **kwargs)
|
||||
|
||||
def write(self, s):
|
||||
if self.additional_out is not None:
|
||||
self.additional_out.write(s)
|
||||
|
||||
super(StdIO, self).write(s)
|
||||
|
||||
# Only for tests to capture std{out,err}
|
||||
def _fake_stdio(additional_out=False):
|
||||
return io.TextIOWrapper(StdIO(additional_out=additional_out))
|
||||
|
||||
def _get_fake_stdio_ucontent(stdio):
|
||||
stdio.flush()
|
||||
stdio.seek(0)
|
||||
return stdio.read()
|
||||
|
||||
def to_utf8(s):
|
||||
return s
|
||||
|
||||
import pickle
|
||||
|
||||
ArgumentParser = argparse.ArgumentParser
|
||||
|
||||
input = input
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# display formatting
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
@ -41,7 +41,7 @@ def bib_oneliner(bibdata):
|
||||
elif bibdata[TYPE_KEY] == 'inproceedings':
|
||||
journal = ' ' + bibdata.get('booktitle', '')
|
||||
|
||||
return sanitize(u'{authors} \"{title}\"{journal}{year}'.format(
|
||||
return sanitize('{authors} \"{title}\"{journal}{year}'.format(
|
||||
authors=color.dye_out(authors, 'author'),
|
||||
title=color.dye_out(bibdata.get('title', ''), 'title'),
|
||||
journal=color.dye_out(journal, 'publisher'),
|
||||
@ -66,6 +66,6 @@ def paper_oneliner(p, citekey_only=False):
|
||||
bibdesc = bib_oneliner(p.bibdata)
|
||||
tags = '' if len(p.tags) == 0 else '| {}'.format(
|
||||
','.join(color.dye_out(t, 'tag') for t in sorted(p.tags)))
|
||||
return u'[{citekey}] {descr} {tags}'.format(
|
||||
return '[{citekey}] {descr} {tags}'.format(
|
||||
citekey=color.dye_out(p.citekey, 'citekey'),
|
||||
descr=bibdesc, tags=tags)
|
||||
|
@ -1,9 +1,9 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import collections
|
||||
from . import uis
|
||||
from . import p3
|
||||
from . import config
|
||||
from . import commands
|
||||
from . import update
|
||||
@ -36,7 +36,7 @@ CORE_CMDS = collections.OrderedDict([
|
||||
def execute(raw_args=sys.argv):
|
||||
|
||||
try:
|
||||
conf_parser = argparse.ArgumentParser(prog="pubs", add_help=False)
|
||||
conf_parser = p3.ArgumentParser(prog="pubs", add_help=False)
|
||||
conf_parser.add_argument("-c", "--config", help="path to config file",
|
||||
type=str, metavar="FILE")
|
||||
conf_parser.add_argument('--force-colors', dest='force_colors',
|
||||
@ -67,8 +67,8 @@ def execute(raw_args=sys.argv):
|
||||
uis.init_ui(conf, force_colors=top_args.force_colors)
|
||||
ui = uis.get_ui()
|
||||
|
||||
parser = argparse.ArgumentParser(description="research papers repository",
|
||||
prog="pubs", add_help=True)
|
||||
parser = p3.ArgumentParser(description="research papers repository",
|
||||
prog="pubs", add_help=True)
|
||||
parser.add_argument('--version', action='version', version=__version__)
|
||||
subparsers = parser.add_subparsers(title="valid commands", dest="command")
|
||||
|
||||
|
25
pubs/uis.py
25
pubs/uis.py
@ -1,4 +1,4 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
@ -42,7 +42,7 @@ def _get_local_editor():
|
||||
return os.environ.get('EDITOR', 'nano')
|
||||
|
||||
|
||||
def _editor_input(editor, initial=u'', suffix='.tmp'):
|
||||
def _editor_input(editor, initial='', suffix='.tmp'):
|
||||
"""Use an editor to get input"""
|
||||
str_initial = initial.encode('utf-8') # TODO: make it a configuration item
|
||||
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file:
|
||||
@ -100,15 +100,19 @@ class PrintUI(object):
|
||||
|
||||
def info(self, message, **kwargs):
|
||||
kwargs['file'] = self._stdout
|
||||
print(u'{}: {}'.format(color.dye_out('info', 'ok'), message), **kwargs)
|
||||
print('{}: {}'.format(color.dye_out('info', 'ok'), message), **kwargs)
|
||||
|
||||
def warning(self, message, **kwargs):
|
||||
kwargs['file'] = self._stderr
|
||||
print(u'{}: {}'.format(color.dye_err('warning', 'warning'), message), **kwargs)
|
||||
print('{}: {}'.format(color.dye_err('warning', 'warning'), message), **kwargs)
|
||||
|
||||
def error(self, message, **kwargs):
|
||||
kwargs['file'] = self._stderr
|
||||
print(u'{}: {}'.format(color.dye_err('error', 'error'), message), **kwargs)
|
||||
print('{}: {}'.format(color.dye_err('error', 'error'), message), **kwargs)
|
||||
# if an exception has been raised and debug is on, raise it.
|
||||
if DEBUG or self.debug:
|
||||
if sys.exc_info()[0] is not None:
|
||||
raise
|
||||
|
||||
def exit(self, error_code=1):
|
||||
sys.exit(error_code)
|
||||
@ -121,6 +125,7 @@ class PrintUI(object):
|
||||
if (not DEBUG) and (not self.debug):
|
||||
self.error(ustr(exc))
|
||||
self.exit()
|
||||
self.error(ustr(exc))
|
||||
return False
|
||||
|
||||
|
||||
@ -136,7 +141,7 @@ class InputUI(PrintUI):
|
||||
try:
|
||||
data = input()
|
||||
except EOFError:
|
||||
self.error(u'Standard input ended while waiting for answer.')
|
||||
self.error('Standard input ended while waiting for answer.')
|
||||
self.exit(1)
|
||||
return ustr(data) #.decode('utf-8')
|
||||
|
||||
@ -159,10 +164,10 @@ class InputUI(PrintUI):
|
||||
if len(set(option_chars)) != len(option_chars): # duplicate chars, char choices are deactivated. #FIXME: should only deactivate ambiguous chars
|
||||
option_chars = []
|
||||
|
||||
option_str = u'/'.join(["{}{}".format(color.dye_out(c, 'bold'), s[1:])
|
||||
option_str = '/'.join(["{}{}".format(color.dye_out(c, 'bold'), s[1:])
|
||||
for c, s in zip(displayed_chars, options)])
|
||||
|
||||
self.message(u'{}: {} {}: '.format(color.dye_err('prompt', 'warning'), question, option_str), end='')
|
||||
self.message('{}: {} {}: '.format(color.dye_err('prompt', 'warning'), question, option_str), end='')
|
||||
while True:
|
||||
answer = self.input()
|
||||
if answer is None or answer == '':
|
||||
@ -176,7 +181,7 @@ class InputUI(PrintUI):
|
||||
return option_chars.index(answer.lower())
|
||||
except ValueError:
|
||||
pass
|
||||
self.message(u'Incorrect option.', option_str)
|
||||
self.message('Incorrect option.', option_str)
|
||||
|
||||
|
||||
def input_choice(self, options, option_chars, default=None, question=''):
|
||||
@ -209,7 +214,7 @@ class InputUI(PrintUI):
|
||||
return option_chars.index(answer.lower())
|
||||
except ValueError:
|
||||
pass
|
||||
self.message(u'Incorrect option.', option_str)
|
||||
self.message('Incorrect option.', option_str)
|
||||
|
||||
def input_yn(self, question='', default='y'):
|
||||
d = 0 if default in (True, 'y', 'yes') else 1
|
||||
|
@ -1,4 +1,6 @@
|
||||
# Function here may belong somewhere else. In the mean time...
|
||||
# Functions here may belong somewhere else. In the mean time...
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from . import color
|
||||
@ -31,7 +33,7 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True):
|
||||
"citekeys:".format(citekey))
|
||||
for c in citekeys:
|
||||
p = repo.pull_paper(c)
|
||||
ui.message(u' {}'.format(pretty.paper_oneliner(p)))
|
||||
ui.message(' {}'.format(pretty.paper_oneliner(p)))
|
||||
if exit_on_fail:
|
||||
ui.exit()
|
||||
return citekey
|
||||
|
@ -21,16 +21,24 @@ real_glob = glob
|
||||
real_io = io
|
||||
|
||||
|
||||
# redirecting output
|
||||
# capture output
|
||||
|
||||
def redirect(f):
|
||||
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
|
||||
stdout = _fake_stdio()
|
||||
stderr = _fake_stdio()
|
||||
sys.stdout, sys.stderr = stdout, stderr
|
||||
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(stdout), _get_fake_stdio_ucontent(stderr)
|
||||
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
|
||||
|
@ -1,4 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import copy
|
||||
@ -18,7 +20,7 @@ class TestGenerateCitekey(unittest.TestCase):
|
||||
def test_escapes_chars(self):
|
||||
doe_bibentry = copy.deepcopy(fixtures.doe_bibentry)
|
||||
citekey, bibdata = bibstruct.get_entry(doe_bibentry)
|
||||
bibdata['author'] = [u'Zôu\\@/ , John']
|
||||
bibdata['author'] = ['Zôu\\@/ , John']
|
||||
key = bibstruct.generate_citekey(doe_bibentry)
|
||||
self.assertEqual(key, 'Zou2013')
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
import yaml
|
||||
@ -89,16 +89,16 @@ class TestEnDecode(unittest.TestCase):
|
||||
|
||||
def test_endecode_keyword_as_keywords(self):
|
||||
decoder = endecoder.EnDecoder()
|
||||
keywords = [u'artificial intelligence', u'Turing test']
|
||||
keywords = ['artificial intelligence', 'Turing test']
|
||||
# Add keywords to bibraw
|
||||
keyword_str = 'keywords = {artificial intelligence, Turing test},\n'
|
||||
biblines = turing_bib.splitlines()
|
||||
biblines.insert(-3, keyword_str)
|
||||
bibsrc = '\n'.join(biblines)
|
||||
entry = decoder.decode_bibdata(bibsrc)['turing1950computing']
|
||||
self.assertNotIn(u'keywords', entry)
|
||||
self.assertIn(u'keyword', entry)
|
||||
self.assertEqual(set(keywords), set(entry[u'keyword']))
|
||||
self.assertNotIn('keywords', entry)
|
||||
self.assertIn('keyword', entry)
|
||||
self.assertEqual(set(keywords), set(entry['keyword']))
|
||||
|
||||
def test_endecode_metadata(self):
|
||||
decoder = endecoder.EnDecoder()
|
||||
@ -110,16 +110,16 @@ class TestEnDecode(unittest.TestCase):
|
||||
decoder = endecoder.EnDecoder()
|
||||
entry = decoder.decode_bibdata(bibtex_raw0)
|
||||
lines = decoder.encode_bibdata(entry).splitlines()
|
||||
self.assertEqual(lines[1].split('=')[0].strip(), u'author')
|
||||
self.assertEqual(lines[2].split('=')[0].strip(), u'title')
|
||||
self.assertEqual(lines[3].split('=')[0].strip(), u'institution')
|
||||
self.assertEqual(lines[4].split('=')[0].strip(), u'publisher')
|
||||
self.assertEqual(lines[5].split('=')[0].strip(), u'year')
|
||||
self.assertEqual(lines[6].split('=')[0].strip(), u'month')
|
||||
self.assertEqual(lines[7].split('=')[0].strip(), u'number')
|
||||
self.assertEqual(lines[8].split('=')[0].strip(), u'url')
|
||||
self.assertEqual(lines[9].split('=')[0].strip(), u'note')
|
||||
self.assertEqual(lines[10].split('=')[0].strip(), u'abstract')
|
||||
self.assertEqual(lines[1].split('=')[0].strip(), 'author')
|
||||
self.assertEqual(lines[2].split('=')[0].strip(), 'title')
|
||||
self.assertEqual(lines[3].split('=')[0].strip(), 'institution')
|
||||
self.assertEqual(lines[4].split('=')[0].strip(), 'publisher')
|
||||
self.assertEqual(lines[5].split('=')[0].strip(), 'year')
|
||||
self.assertEqual(lines[6].split('=')[0].strip(), 'month')
|
||||
self.assertEqual(lines[7].split('=')[0].strip(), 'number')
|
||||
self.assertEqual(lines[8].split('=')[0].strip(), 'url')
|
||||
self.assertEqual(lines[9].split('=')[0].strip(), 'note')
|
||||
self.assertEqual(lines[10].split('=')[0].strip(), 'abstract')
|
||||
|
||||
def test_endecode_link_as_url(self):
|
||||
decoder = endecoder.EnDecoder()
|
||||
@ -129,9 +129,9 @@ class TestEnDecode(unittest.TestCase):
|
||||
raw_with_link = bibtex_raw0.replace('url = ', 'link = ')
|
||||
entry = decoder.decode_bibdata(raw_with_link)
|
||||
lines = decoder.encode_bibdata(entry).splitlines()
|
||||
self.assertEqual(lines[8].split('=')[0].strip(), u'url')
|
||||
self.assertEqual(lines[8].split('=')[0].strip(), 'url')
|
||||
self.assertEqual(lines[8].split('=')[1].strip(),
|
||||
u'{http://ilpubs.stanford.edu:8090/422/},')
|
||||
'{http://ilpubs.stanford.edu:8090/422/},')
|
||||
|
||||
def test_endecode_bibtex_ignores_fields(self):
|
||||
decoder = endecoder.EnDecoder()
|
||||
|
@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
import os
|
||||
|
||||
@ -19,14 +20,14 @@ class TestPretty(unittest.TestCase):
|
||||
def test_oneliner(self):
|
||||
decoder = endecoder.EnDecoder()
|
||||
bibdata = decoder.decode_bibdata(bibtex_raw0)
|
||||
line = u'Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999)'
|
||||
line = 'Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999)'
|
||||
self.assertEqual(color.undye(pretty.bib_oneliner(bibdata['Page99'])), line)
|
||||
|
||||
def test_oneliner_no_year(self):
|
||||
decoder = endecoder.EnDecoder()
|
||||
bibdata = decoder.decode_bibdata(bibtex_raw0)
|
||||
bibdata['Page99'].pop('year')
|
||||
line = u'Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web."'
|
||||
line = 'Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web."'
|
||||
self.assertEqual(color.undye(pretty.bib_oneliner(bibdata['Page99'])), line)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -116,22 +116,24 @@ class CommandTestCase(fake_env.TestFakeFs):
|
||||
input.as_global()
|
||||
try:
|
||||
if capture_output:
|
||||
_, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(
|
||||
actual_cmd.split()
|
||||
_, stdout, stderr = fake_env.capture(pubs_cmd.execute,
|
||||
verbose=PRINT_OUTPUT)(
|
||||
actual_cmd.split())
|
||||
actual_out = color.undye(stdout)
|
||||
actual_err = color.undye(stderr)
|
||||
if expected_out is not None:
|
||||
self.assertEqual(actual_out, expected_out)
|
||||
self.assertEqual(p3.u_maybe(actual_out), p3.u_maybe(expected_out))
|
||||
#self.assertEqual(actual_out, expected_out)
|
||||
if expected_err is not None:
|
||||
self.assertEqual(actual_err, expected_err)
|
||||
self.assertEqual(p3.u_maybe(actual_err), p3.u_maybe(expected_err))
|
||||
outs.append(color.undye(actual_out))
|
||||
else:
|
||||
pubs_cmd.execute(actual_cmd.split())
|
||||
except fake_env.FakeInput.UnexpectedInput:
|
||||
except fake_env.FakeInput.UnexpectedInput as e:
|
||||
self.fail('Unexpected input asked by command: {}.'.format(
|
||||
actual_cmd))
|
||||
if PRINT_OUTPUT:
|
||||
print(outs)
|
||||
return outs
|
||||
except SystemExit as exc:
|
||||
exc_class, exc, tb = sys.exc_info()
|
||||
@ -200,7 +202,6 @@ class TestAlone(CommandTestCase):
|
||||
self.execute_cmds(['pubs'])
|
||||
self.assertEqual(cm.exception.code, 2)
|
||||
|
||||
|
||||
def test_alone_prints_help(self):
|
||||
# capturing the output of `pubs --help` is difficult because argparse
|
||||
# raises as SystemExit(0) after calling `print_help`, and this gets
|
||||
|
Loading…
x
Reference in New Issue
Block a user