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.
main
Fabien C. Y. Benureau 7 years ago
parent 38133fc053
commit dc4e118c3c

@ -25,14 +25,14 @@ def str2citekey(s):
def check_citekey(citekey): def check_citekey(citekey):
if citekey is None or not citekey.strip(): 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): def verify_bibdata(bibdata):
if bibdata is None or len(bibdata) == 0: if bibdata is None or len(bibdata) == 0:
raise ValueError(u"no valid bibdata") raise ValueError("no valid bibdata")
if len(bibdata) > 1: 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): def get_entry(bibdata):
@ -64,12 +64,12 @@ def generate_citekey(bibdata):
first_author = entry[author_key][0] first_author = entry[author_key][0]
except KeyError: except KeyError:
raise ValueError( raise ValueError(
u"No author or editor defined: cannot generate a citekey.") "No author or editor defined: cannot generate a citekey.")
try: try:
year = entry['year'] year = entry['year']
except KeyError: except KeyError:
year = '' year = ''
citekey = u'{}{}'.format(u''.join(author_last(first_author)), year) citekey = '{}{}'.format(''.join(author_last(first_author)), year)
return str2citekey(citekey) return str2citekey(citekey)

@ -1,8 +1,6 @@
""" """
Code to handle colored text Code to handle colored text
"""
"""
Here is a little explanation about bash color code, useful to understand Here is a little explanation about bash color code, useful to understand
the code below. See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html the code below. See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
for a complete referece. 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 display colors, with 0 <= c < 8 corresponding to the 8 above colors, and
8 <= c < 16 their bright version. 8 <= c < 16 their bright version.
""" """
from __future__ import unicode_literals
import sys import sys
import re import re
@ -32,15 +31,15 @@ import os
import subprocess import subprocess
COLOR_LIST = {u'black': '0', u'red': '1', u'green': '2', u'yellow': '3', u'blue': '4', COLOR_LIST = {'black': '0', 'red': '1', 'green': '2', 'yellow': '3', 'blue': '4',
u'magenta': '5', u'cyan': '6', u'grey': '7', 'magenta': '5', 'cyan': '6', 'grey': '7',
u'brightblack': '8', u'brightred': '9', u'brightgreen': '10', 'brightblack': '8', 'brightred': '9', 'brightgreen': '10',
u'brightyellow': '11', u'brightblue': '12', u'brightmagenta': '13', 'brightyellow': '11', 'brightblue': '12', 'brightmagenta': '13',
u'brightcyan': '14', u'brightgrey': '15', 'brightcyan': '14', 'brightgrey': '15',
u'darkgrey': '8', # == brightblack 'darkgrey': '8', # == brightblack
u'gray': '7', u'darkgray': '8', u'brightgray': '15', # gray/grey spelling 'gray': '7', 'darkgray': '8', 'brightgray': '15', # gray/grey spelling
u'purple': '5', # for compatibility reasons 'purple': '5', # for compatibility reasons
u'white': '15' # == brightgrey 'white': '15' # == brightgrey
} }
for c in range(256): for c in range(256):
COLOR_LIST[str(c)] = str(c) 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. normal colors.
:param italic: generate italic 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(): for name, code in COLOR_LIST.items():
colors[name] = u'' colors[name] = ''
colors[u'b' +name] = u'' colors['b' +name] = ''
colors[u'i' +name] = u'' colors['i' +name] = ''
colors[u'bi'+name] = u'' colors['bi'+name] = ''
color_support = _color_supported(stream, force=force_colors) >= 8 color_support = _color_supported(stream, force=force_colors) >= 8
if (color or bold or italic) and color_support: if (color or bold or italic) and color_support:
bold_flag, italic_flag = '', '' bold_flag, italic_flag = '', ''
if bold: if bold:
colors['bold'] = u'\033[1m' colors['bold'] = '\033[1m'
bold_flag = '1;' bold_flag = '1;'
if italic: if italic:
colors['italic'] = u'\033[3m' colors['italic'] = '\033[3m'
italic_flag = '3;' italic_flag = '3;'
if bold and italic: if bold and italic:
colors['bolditalic'] = u'\033[1;3m' colors['bolditalic'] = '\033[1;3m'
for name, code in COLOR_LIST.items(): for name, code in COLOR_LIST.items():
if color: if color:
colors[name] = u'\033[38;5;{}m'.format(code) colors[name] = '\033[38;5;{}m'.format(code)
colors[u'b'+name] = u'\033[{}38;5;{}m'.format(bold_flag, code) colors['b'+name] = '\033[{}38;5;{}m'.format(bold_flag, code)
colors[u'i'+name] = u'\033[{}38;5;{}m'.format(italic_flag, code) colors['i'+name] = '\033[{}38;5;{}m'.format(italic_flag, code)
colors[u'bi'+name] = u'\033[{}38;5;{}m'.format(bold_flag, italic_flag, code) colors['bi'+name] = '\033[{}38;5;{}m'.format(bold_flag, italic_flag, code)
else: else:
if bold: 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: 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: 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: if color or bold or italic:
colors[u'end'] = u'\033[0m' colors['end'] = '\033[0m'
return colors return colors
@ -121,11 +120,11 @@ COLORS_ERR = generate_colors(sys.stderr, color=False, bold=False, italic=False)
def dye_out(s, color='end'): def dye_out(s, color='end'):
"""Color a string for output on stdout""" """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'): def dye_err(s, color='end'):
"""Color a string for output on stderr""" """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): def setup(conf, force_colors=False):

@ -1,5 +1,8 @@
from __future__ import unicode_literals
import argparse import argparse
from ..uis import get_ui from ..uis import get_ui
from .. import p3
from .. import bibstruct from .. import bibstruct
from .. import content from .. import content
from .. import repo from .. import repo
@ -29,7 +32,7 @@ def parser(subparsers, conf):
default=None default=None
).completer = CommaSeparatedTagsCompletion(conf) ).completer = CommaSeparatedTagsCompletion(conf)
parser.add_argument('-k', '--citekey', help='citekey associated with the paper;\nif not provided, one will be generated automatically.', 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, parser.add_argument('-L', '--link', action='store_false', dest='copy', default=True,
help="don't copy document files, just create a link.") help="don't copy document files, just create a link.")
parser.add_argument('-M', '--move', action='store_true', dest='move', default=False, 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 uis
from .. import config from .. import config
from .. import content from .. import content

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import os import os
import subprocess import subprocess

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from ..paper import Paper from ..paper import Paper
from .. import repo from .. import repo

@ -1,4 +1,4 @@
from __future__ import print_function from __future__ import unicode_literals
import argparse import argparse

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import os import os
import datetime import datetime
@ -78,10 +80,10 @@ def command(conf, args):
for k in keys: for k in keys:
p = papers[k] p = papers[k]
if isinstance(p, Exception): 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: else:
rp.push_paper(p, overwrite=args.overwrite) 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) docfile = bibstruct.extract_docfile(p.bibdata)
if docfile is None: if docfile is None:
ui.warning("No file for {}.".format(p.citekey)) ui.warning("No file for {}.".format(p.citekey))

@ -1,4 +1,5 @@
# init command # init command
from __future__ import unicode_literals
import os import os

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
from .. import repo from .. import repo

@ -7,7 +7,7 @@ from ..completion import CiteKeyCompletion
def parser(subparsers, conf): def parser(subparsers, conf):
parser = subparsers.add_parser('note', parser = subparsers.add_parser('note',
help='edit the note attached to a paper') help='edit the note attached to a paper')
parser.add_argument('citekey', help='citekey of the paper' parser.add_argument('citekey', help='citekey of the paper',
).completer = CiteKeyCompletion(conf) ).completer = CiteKeyCompletion(conf)
return parser return parser

@ -1,8 +1,10 @@
from __future__ import unicode_literals
from .. import repo from .. import repo
from .. import color from .. import color
from ..uis import get_ui from ..uis import get_ui
from ..utils import resolve_citekey_list from ..utils import resolve_citekey_list
from ..p3 import ustr from ..p3 import ustr, to_utf8
from ..completion import CiteKeyCompletion from ..completion import CiteKeyCompletion
@ -10,8 +12,8 @@ def parser(subparsers, conf):
parser = subparsers.add_parser('remove', help='removes a publication') parser = subparsers.add_parser('remove', help='removes a publication')
parser.add_argument('-f', '--force', action='store_true', default=None, parser.add_argument('-f', '--force', action='store_true', default=None,
help="does not prompt for confirmation.") help="does not prompt for confirmation.")
parser.add_argument('citekeys', nargs='+', parser.add_argument('citekeys', nargs='+', type=to_utf8,
help="one or several citekeys" help="one or several citekeys",
).completer = CiteKeyCompletion(conf) ).completer = CiteKeyCompletion(conf)
return parser return parser
@ -21,6 +23,7 @@ def command(conf, args):
ui = get_ui() ui = get_ui()
force = args.force force = args.force
rp = repo.Repository(conf) 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) 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: except Exception as e:
ui.error(ustr(e)) ui.error(ustr(e))
failed = True failed = True
ui.message('The publication(s) [{}] were removed'.format(
', '.join([color.dye_out(c, 'citekey') for c in keys])))
if failed: if failed:
ui.exit() # Exit with nonzero error code 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. # FIXME: print should check that removal proceeded well.
else: else:
ui.message('The publication(s) [{}] were {} removed'.format( ui.message('The publication(s) [{}] were {} removed'.format(

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from ..uis import get_ui from ..uis import get_ui
from .. import color from .. import color
from .. import repo from .. import repo

@ -16,6 +16,7 @@ The different use cases are :
7. > pubs tag -war+math+romance 7. > pubs tag -war+math+romance
display all papers with the tag 'math', 'romance' but not 'war' display all papers with the tag 'math', 'romance' but not 'war'
""" """
from __future__ import unicode_literals
import re import re

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import webbrowser import webbrowser
from .. import p3 from .. import p3

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import sys import sys
import os import os
import shutil import shutil
@ -28,7 +30,7 @@ class UnableToDecodeTextFile(Exception):
def _check_system_path_exists(path, fail=True): def _check_system_path_exists(path, fail=True):
answer = os.path.exists(path) answer = os.path.exists(path)
if not answer and fail: if not answer and fail:
raise IOError(u'File does not exist: {}'.format(path)) raise IOError('File does not exist: {}'.format(path))
else: else:
return answer return answer
@ -37,7 +39,7 @@ def _check_system_path_is(nature, path, fail=True):
check_fun = getattr(os.path, nature) check_fun = getattr(os.path, nature)
answer = check_fun(path) answer = check_fun(path)
if not answer and fail: if not answer and fail:
raise IOError(u'{} is not a {}.'.format(path, nature)) raise IOError('{} is not a {}.'.format(path, nature))
else: else:
return answer return answer
@ -56,13 +58,13 @@ def _open(path, mode):
def check_file(path, fail=True): def check_file(path, fail=True):
syspath = system_path(path) syspath = system_path(path)
return (_check_system_path_exists(syspath, fail=fail) and 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): def check_directory(path, fail=True):
syspath = system_path(path) syspath = system_path(path)
return (_check_system_path_exists(syspath, fail=fail) and 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): def read_text_file(filepath, fail=True):
@ -112,23 +114,23 @@ def write_file(filepath, data, mode='w'):
def content_type(path): def content_type(path):
parsed = urlparse(path) parsed = urlparse(path)
if parsed.scheme == u'http': if parsed.scheme == 'http':
return u'url' return 'url'
else: else:
return u'file' return 'file'
def url_exists(url): def url_exists(url):
parsed = urlparse(url) parsed = urlparse(url)
conn = HTTPConnection(parsed.netloc) conn = HTTPConnection(parsed.netloc)
conn.request(u'HEAD', parsed.path) conn.request('HEAD', parsed.path)
response = conn.getresponse() response = conn.getresponse()
conn.close() conn.close()
return response.status == 200 return response.status == 200
def check_content(path): def check_content(path):
if content_type(path) == u'url': if content_type(path) == 'url':
return url_exists(path) return url_exists(path)
else: else:
return check_file(path) return check_file(path)
@ -136,7 +138,7 @@ def check_content(path):
def _get_byte_url_content(path, ui=None): def _get_byte_url_content(path, ui=None):
if ui is not None: if ui is not None:
ui.message(u'dowloading {}'.format(path)) ui.message('dowloading {}'.format(path))
response = urlopen(path) response = urlopen(path)
return response.read() return response.read()
@ -151,7 +153,7 @@ def _dump_byte_url_content(source, target):
def get_content(path, ui=None): def get_content(path, ui=None):
"""Will be useful when we need to get content from url""" """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') return _get_byte_url_content(path, ui=ui).decode(encoding='utf-8')
else: else:
return read_text_file(path) return read_text_file(path)
@ -163,19 +165,19 @@ def move_content(source, target, overwrite=False):
if source == target: if source == target:
return return
if not overwrite and os.path.exists(target): if not overwrite and os.path.exists(target):
raise IOError(u'target file exists') raise IOError('target file exists')
shutil.move(source, target) shutil.move(source, target)
def copy_content(source, target, overwrite=False): 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: if not source_is_url:
source = system_path(source) source = system_path(source)
target = system_path(target) target = system_path(target)
if source == target: if source == target:
return return
if not overwrite and os.path.exists(target): 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: if source_is_url:
_dump_byte_url_content(source, target) _dump_byte_url_content(source, target)
else: else:

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from . import filebroker from . import filebroker
from . import endecoder from . import endecoder
from .p3 import pickle from .p3 import pickle
@ -76,7 +78,7 @@ class DataBroker(object):
def verify(self, bibdata_raw): def verify(self, bibdata_raw):
"""Will return None if bibdata_raw can't be decoded""" """Will return None if bibdata_raw can't be decoded"""
try: try:
if bibdata_raw.startswith(u'\ufeff'): if bibdata_raw.startswith('\ufeff'):
# remove BOM, because bibtexparser does not support it. # remove BOM, because bibtexparser does not support it.
bibdata_raw = bibdata_raw[1:] bibdata_raw = bibdata_raw[1:]
return self.endecoder.decode_bibdata(bibdata_raw) return self.endecoder.decode_bibdata(bibdata_raw)

@ -1,5 +1,4 @@
from __future__ import (print_function, absolute_import, division, from __future__ import absolute_import, unicode_literals
unicode_literals)
import copy import copy

@ -1,6 +1,6 @@
import os import os
import re import re
from .p3 import urlparse from .p3 import urlparse, u_maybe
from .content import (check_file, check_directory, read_text_file, write_file, from .content import (check_file, check_directory, read_text_file, write_file,
system_path, check_content, copy_content) system_path, check_content, copy_content)
@ -18,7 +18,7 @@ def filter_filename(filename, ext):
""" """
pattern = '.*\{}$'.format(ext) pattern = '.*\{}$'.format(ext)
if re.match(pattern, filename) is not None: if re.match(pattern, filename) is not None:
return filename[:-len(ext)] return u_maybe(filename[:-len(ext)])
class FileBroker(object): class FileBroker(object):

@ -1,5 +1,10 @@
import io import io
import sys import sys
import argparse
from six import b, u
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
import cPickle as pickle import cPickle as pickle
@ -21,12 +26,46 @@ if sys.version_info[0] == 2:
from urllib2 import urlopen from urllib2 import urlopen
from httplib import HTTPConnection from httplib import HTTPConnection
file = None 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): def _get_fake_stdio_ucontent(stdio):
ustdio = io.TextIOWrapper(stdio)
ustdio.seek(0) # ustdio = io.TextIOWrapper(stdio)
return ustdio.read() 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: else:
ustr = str ustr = str
@ -43,16 +82,37 @@ else:
def _get_raw_stderr(): def _get_raw_stderr():
return sys.stderr.buffer return sys.stderr.buffer
def _fake_stdio(): def u_maybe(s):
return io.TextIOWrapper(io.BytesIO()) # Only for tests to capture std{out,err} 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): def _get_fake_stdio_ucontent(stdio):
stdio.flush() stdio.flush()
stdio.seek(0) stdio.seek(0)
return stdio.read() return stdio.read()
def to_utf8(s):
return s
import pickle import pickle
ArgumentParser = argparse.ArgumentParser
input = input input = input

@ -1,4 +1,4 @@
# display formatting from __future__ import unicode_literals
import re import re
@ -41,7 +41,7 @@ def bib_oneliner(bibdata):
elif bibdata[TYPE_KEY] == 'inproceedings': elif bibdata[TYPE_KEY] == 'inproceedings':
journal = ' ' + bibdata.get('booktitle', '') 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'), authors=color.dye_out(authors, 'author'),
title=color.dye_out(bibdata.get('title', ''), 'title'), title=color.dye_out(bibdata.get('title', ''), 'title'),
journal=color.dye_out(journal, 'publisher'), journal=color.dye_out(journal, 'publisher'),
@ -66,6 +66,6 @@ def paper_oneliner(p, citekey_only=False):
bibdesc = bib_oneliner(p.bibdata) bibdesc = bib_oneliner(p.bibdata)
tags = '' if len(p.tags) == 0 else '| {}'.format( tags = '' if len(p.tags) == 0 else '| {}'.format(
','.join(color.dye_out(t, 'tag') for t in sorted(p.tags))) ','.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'), citekey=color.dye_out(p.citekey, 'citekey'),
descr=bibdesc, tags=tags) descr=bibdesc, tags=tags)

@ -1,9 +1,9 @@
# PYTHON_ARGCOMPLETE_OK # PYTHON_ARGCOMPLETE_OK
import sys import sys
import argparse
import collections import collections
from . import uis from . import uis
from . import p3
from . import config from . import config
from . import commands from . import commands
from . import update from . import update
@ -36,7 +36,7 @@ CORE_CMDS = collections.OrderedDict([
def execute(raw_args=sys.argv): def execute(raw_args=sys.argv):
try: 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", conf_parser.add_argument("-c", "--config", help="path to config file",
type=str, metavar="FILE") type=str, metavar="FILE")
conf_parser.add_argument('--force-colors', dest='force_colors', 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) uis.init_ui(conf, force_colors=top_args.force_colors)
ui = uis.get_ui() ui = uis.get_ui()
parser = argparse.ArgumentParser(description="research papers repository", parser = p3.ArgumentParser(description="research papers repository",
prog="pubs", add_help=True) prog="pubs", add_help=True)
parser.add_argument('--version', action='version', version=__version__) parser.add_argument('--version', action='version', version=__version__)
subparsers = parser.add_subparsers(title="valid commands", dest="command") subparsers = parser.add_subparsers(title="valid commands", dest="command")

@ -1,4 +1,4 @@
from __future__ import print_function from __future__ import print_function, unicode_literals
import os import os
import sys import sys
@ -42,7 +42,7 @@ def _get_local_editor():
return os.environ.get('EDITOR', 'nano') 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""" """Use an editor to get input"""
str_initial = initial.encode('utf-8') # TODO: make it a configuration item str_initial = initial.encode('utf-8') # TODO: make it a configuration item
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file: with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file:
@ -100,15 +100,19 @@ class PrintUI(object):
def info(self, message, **kwargs): def info(self, message, **kwargs):
kwargs['file'] = self._stdout 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): def warning(self, message, **kwargs):
kwargs['file'] = self._stderr 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): def error(self, message, **kwargs):
kwargs['file'] = self._stderr 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): def exit(self, error_code=1):
sys.exit(error_code) sys.exit(error_code)
@ -121,6 +125,7 @@ class PrintUI(object):
if (not DEBUG) and (not self.debug): if (not DEBUG) and (not self.debug):
self.error(ustr(exc)) self.error(ustr(exc))
self.exit() self.exit()
self.error(ustr(exc))
return False return False
@ -136,7 +141,7 @@ class InputUI(PrintUI):
try: try:
data = input() data = input()
except EOFError: except EOFError:
self.error(u'Standard input ended while waiting for answer.') self.error('Standard input ended while waiting for answer.')
self.exit(1) self.exit(1)
return ustr(data) #.decode('utf-8') 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 if len(set(option_chars)) != len(option_chars): # duplicate chars, char choices are deactivated. #FIXME: should only deactivate ambiguous chars
option_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)]) 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: while True:
answer = self.input() answer = self.input()
if answer is None or answer == '': if answer is None or answer == '':
@ -176,7 +181,7 @@ class InputUI(PrintUI):
return option_chars.index(answer.lower()) return option_chars.index(answer.lower())
except ValueError: except ValueError:
pass pass
self.message(u'Incorrect option.', option_str) self.message('Incorrect option.', option_str)
def input_choice(self, options, option_chars, default=None, question=''): def input_choice(self, options, option_chars, default=None, question=''):
@ -209,7 +214,7 @@ class InputUI(PrintUI):
return option_chars.index(answer.lower()) return option_chars.index(answer.lower())
except ValueError: except ValueError:
pass pass
self.message(u'Incorrect option.', option_str) self.message('Incorrect option.', option_str)
def input_yn(self, question='', default='y'): def input_yn(self, question='', default='y'):
d = 0 if default in (True, 'y', 'yes') else 1 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 import re
from . import color from . import color
@ -31,7 +33,7 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True):
"citekeys:".format(citekey)) "citekeys:".format(citekey))
for c in citekeys: for c in citekeys:
p = repo.pull_paper(c) p = repo.pull_paper(c)
ui.message(u' {}'.format(pretty.paper_oneliner(p))) ui.message(' {}'.format(pretty.paper_oneliner(p)))
if exit_on_fail: if exit_on_fail:
ui.exit() ui.exit()
return citekey return citekey

@ -21,16 +21,24 @@ real_glob = glob
real_io = io 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): def newf(*args, **kwargs):
old_stderr, old_stdout = sys.stderr, sys.stdout old_stderr, old_stdout = sys.stderr, sys.stdout
stdout = _fake_stdio() sys.stdout = _fake_stdio(additional_out=old_stderr if verbose else None)
stderr = _fake_stdio() sys.stderr = _fake_stdio(additional_out=old_stderr if False else None)
sys.stdout, sys.stderr = stdout, stderr
try: 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: finally:
sys.stderr, sys.stdout = old_stderr, old_stdout sys.stderr, sys.stdout = old_stderr, old_stdout
return newf return newf

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os import os
import unittest import unittest
import copy import copy
@ -18,7 +20,7 @@ class TestGenerateCitekey(unittest.TestCase):
def test_escapes_chars(self): def test_escapes_chars(self):
doe_bibentry = copy.deepcopy(fixtures.doe_bibentry) doe_bibentry = copy.deepcopy(fixtures.doe_bibentry)
citekey, bibdata = bibstruct.get_entry(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) key = bibstruct.generate_citekey(doe_bibentry)
self.assertEqual(key, 'Zou2013') self.assertEqual(key, 'Zou2013')

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- from __future__ import unicode_literals
from __future__ import print_function
import unittest import unittest
import yaml import yaml
@ -89,16 +89,16 @@ class TestEnDecode(unittest.TestCase):
def test_endecode_keyword_as_keywords(self): def test_endecode_keyword_as_keywords(self):
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()
keywords = [u'artificial intelligence', u'Turing test'] keywords = ['artificial intelligence', 'Turing test']
# Add keywords to bibraw # Add keywords to bibraw
keyword_str = 'keywords = {artificial intelligence, Turing test},\n' keyword_str = 'keywords = {artificial intelligence, Turing test},\n'
biblines = turing_bib.splitlines() biblines = turing_bib.splitlines()
biblines.insert(-3, keyword_str) biblines.insert(-3, keyword_str)
bibsrc = '\n'.join(biblines) bibsrc = '\n'.join(biblines)
entry = decoder.decode_bibdata(bibsrc)['turing1950computing'] entry = decoder.decode_bibdata(bibsrc)['turing1950computing']
self.assertNotIn(u'keywords', entry) self.assertNotIn('keywords', entry)
self.assertIn(u'keyword', entry) self.assertIn('keyword', entry)
self.assertEqual(set(keywords), set(entry[u'keyword'])) self.assertEqual(set(keywords), set(entry['keyword']))
def test_endecode_metadata(self): def test_endecode_metadata(self):
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()
@ -110,16 +110,16 @@ class TestEnDecode(unittest.TestCase):
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()
entry = decoder.decode_bibdata(bibtex_raw0) entry = decoder.decode_bibdata(bibtex_raw0)
lines = decoder.encode_bibdata(entry).splitlines() lines = decoder.encode_bibdata(entry).splitlines()
self.assertEqual(lines[1].split('=')[0].strip(), u'author') self.assertEqual(lines[1].split('=')[0].strip(), 'author')
self.assertEqual(lines[2].split('=')[0].strip(), u'title') self.assertEqual(lines[2].split('=')[0].strip(), 'title')
self.assertEqual(lines[3].split('=')[0].strip(), u'institution') self.assertEqual(lines[3].split('=')[0].strip(), 'institution')
self.assertEqual(lines[4].split('=')[0].strip(), u'publisher') self.assertEqual(lines[4].split('=')[0].strip(), 'publisher')
self.assertEqual(lines[5].split('=')[0].strip(), u'year') self.assertEqual(lines[5].split('=')[0].strip(), 'year')
self.assertEqual(lines[6].split('=')[0].strip(), u'month') self.assertEqual(lines[6].split('=')[0].strip(), 'month')
self.assertEqual(lines[7].split('=')[0].strip(), u'number') self.assertEqual(lines[7].split('=')[0].strip(), 'number')
self.assertEqual(lines[8].split('=')[0].strip(), u'url') self.assertEqual(lines[8].split('=')[0].strip(), 'url')
self.assertEqual(lines[9].split('=')[0].strip(), u'note') self.assertEqual(lines[9].split('=')[0].strip(), 'note')
self.assertEqual(lines[10].split('=')[0].strip(), u'abstract') self.assertEqual(lines[10].split('=')[0].strip(), 'abstract')
def test_endecode_link_as_url(self): def test_endecode_link_as_url(self):
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()
@ -129,9 +129,9 @@ class TestEnDecode(unittest.TestCase):
raw_with_link = bibtex_raw0.replace('url = ', 'link = ') raw_with_link = bibtex_raw0.replace('url = ', 'link = ')
entry = decoder.decode_bibdata(raw_with_link) entry = decoder.decode_bibdata(raw_with_link)
lines = decoder.encode_bibdata(entry).splitlines() 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(), 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): def test_endecode_bibtex_ignores_fields(self):
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- from __future__ import unicode_literals
import unittest import unittest
import os import os
@ -19,14 +20,14 @@ class TestPretty(unittest.TestCase):
def test_oneliner(self): def test_oneliner(self):
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()
bibdata = decoder.decode_bibdata(bibtex_raw0) 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) self.assertEqual(color.undye(pretty.bib_oneliner(bibdata['Page99'])), line)
def test_oneliner_no_year(self): def test_oneliner_no_year(self):
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()
bibdata = decoder.decode_bibdata(bibtex_raw0) bibdata = decoder.decode_bibdata(bibtex_raw0)
bibdata['Page99'].pop('year') 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) self.assertEqual(color.undye(pretty.bib_oneliner(bibdata['Page99'])), line)
if __name__ == '__main__': if __name__ == '__main__':

@ -116,22 +116,24 @@ class CommandTestCase(fake_env.TestFakeFs):
input.as_global() input.as_global()
try: try:
if capture_output: 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_cmd.split())
actual_out = color.undye(stdout) actual_out = color.undye(stdout)
actual_err = color.undye(stderr) actual_err = color.undye(stderr)
if expected_out is not None: 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: if expected_err is not None:
self.assertEqual(actual_err, expected_err) self.assertEqual(actual_err, expected_err)
self.assertEqual(p3.u_maybe(actual_err), p3.u_maybe(expected_err))
outs.append(color.undye(actual_out)) outs.append(color.undye(actual_out))
else: else:
pubs_cmd.execute(actual_cmd.split()) 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( self.fail('Unexpected input asked by command: {}.'.format(
actual_cmd)) actual_cmd))
if PRINT_OUTPUT:
print(outs)
return outs return outs
except SystemExit as exc: except SystemExit as exc:
exc_class, exc, tb = sys.exc_info() exc_class, exc, tb = sys.exc_info()
@ -200,7 +202,6 @@ class TestAlone(CommandTestCase):
self.execute_cmds(['pubs']) self.execute_cmds(['pubs'])
self.assertEqual(cm.exception.code, 2) self.assertEqual(cm.exception.code, 2)
def test_alone_prints_help(self): def test_alone_prints_help(self):
# capturing the output of `pubs --help` is difficult because argparse # capturing the output of `pubs --help` is difficult because argparse
# raises as SystemExit(0) after calling `print_help`, and this gets # raises as SystemExit(0) after calling `print_help`, and this gets

Loading…
Cancel
Save