new print ui, more robust colors
This commit is contained in:
parent
d5a4fcf73c
commit
cce9406670
@ -54,7 +54,6 @@ def generate_citekey(bibdata):
|
|||||||
:raise ValueError: if no author nor editor is defined.
|
:raise ValueError: if no author nor editor is defined.
|
||||||
"""
|
"""
|
||||||
citekey, entry = get_entry(bibdata)
|
citekey, entry = get_entry(bibdata)
|
||||||
|
|
||||||
author_key = 'author' if 'author' in entry else 'editor'
|
author_key = 'author' if 'author' in entry else 'editor'
|
||||||
try:
|
try:
|
||||||
first_author = entry[author_key][0]
|
first_author = entry[author_key][0]
|
||||||
|
@ -1,47 +1,90 @@
|
|||||||
"""
|
"""
|
||||||
Small code to handle colored text
|
Small code to handle colored text
|
||||||
"""
|
"""
|
||||||
|
import sys
|
||||||
import re
|
import re
|
||||||
|
|
||||||
bold = '\033[1m'
|
def _color_supported(stream):
|
||||||
end = '\033[0m'
|
"""Returns True is the stream supports colors"""
|
||||||
|
if sys.platform == 'win32' and 'ANSICON' not in os.environ:
|
||||||
|
return False
|
||||||
|
if hasattr(stream, 'isatty') and stream.isatty(): # we have a tty
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
curses.setupterm()
|
||||||
|
return curses.tigetnum('colors') >= 8
|
||||||
|
except Exception: # not picky.
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
black = '\033[0;30m'
|
COLOR_LIST = [u'black', u'red', u'green', u'yellow', u'blue', u'purple', u'cyan', u'grey']
|
||||||
red = '\033[0;31m'
|
|
||||||
green = '\033[0;32m'
|
|
||||||
yellow = '\033[0;33m'
|
|
||||||
blue = '\033[0;34m'
|
|
||||||
purple = '\033[0;35m'
|
|
||||||
cyan = '\033[0;36m'
|
|
||||||
grey = '\033[0;37m'
|
|
||||||
|
|
||||||
ok = green
|
def generate_colors(stream, color=True, bold=True, italic=True):
|
||||||
error = red
|
colors = {name: u'' for name in COLOR_LIST}
|
||||||
normal = grey
|
colors.update({u'b' +name: u'' for name in COLOR_LIST})
|
||||||
citekey = purple
|
colors.update({u'i' +name: u'' for name in COLOR_LIST})
|
||||||
filepath = bold
|
colors.update({u'bi'+name: u'' for name in COLOR_LIST})
|
||||||
tag = cyan
|
colors[u'bold'] = u''
|
||||||
|
colors[u'italic'] = u''
|
||||||
|
colors[u'end'] = u''
|
||||||
|
|
||||||
def dye(s, color=end, bold=False):
|
if (color or bold or italic) and _color_supported(stream):
|
||||||
assert color[0] == '\033'
|
bold_flag, italic_flag = '', ''
|
||||||
if bold:
|
if bold:
|
||||||
color = '\033[1' + color[3:]
|
colors['bold'] = u'\x1b[1m'
|
||||||
return color + s + end
|
bold_flag = '1;'
|
||||||
|
if italic:
|
||||||
|
colors['italic'] = u'\x1b[3m'
|
||||||
|
italic_flag = '3;'
|
||||||
|
|
||||||
|
for i, name in enumerate(COLOR_LIST):
|
||||||
|
if color:
|
||||||
|
color_flag = '3{}'.format(name)
|
||||||
|
colors[name] = u'\x1b[{}m'.format(color_flag)
|
||||||
|
colors.update({u'b'+name: u'\x1b[{}3{}m'.format(bold_flag, i) for i, name in enumerate(COLOR_LIST)})
|
||||||
|
colors.update({u'i'+name: u'\x1b[{}3{}m'.format(italic_flag, i) for i, name in enumerate(COLOR_LIST)})
|
||||||
|
colors.update({u'bi'+name: u'\x1b[{}3{}m'.format(bold_flag, italic_flag, i) for i, name in enumerate(COLOR_LIST)})
|
||||||
|
else:
|
||||||
|
if bold:
|
||||||
|
colors.update({u'b'+name: u'\x1b[{}m'.format(bold_flag, i) for i, name in enumerate(COLOR_LIST)})
|
||||||
|
if italic:
|
||||||
|
colors.update({u'i'+name: u'\x1b[{}m'.format(italic_flag, i) for i, name in enumerate(COLOR_LIST)})
|
||||||
|
if bold or italic:
|
||||||
|
colors.update({u'bi'+name: u'\x1b[{}m'.format(bold_flag, italic_flag, i) for i, name in enumerate(COLOR_LIST)})
|
||||||
|
|
||||||
|
if color or bold or italic:
|
||||||
|
colors[u'end'] = u'\x1b[0m'
|
||||||
|
|
||||||
|
return colors
|
||||||
|
|
||||||
|
|
||||||
|
COLORS_OUT = generate_colors(sys.stdout, color=True, bold=True, italic=True)
|
||||||
|
COLORS_ERR = generate_colors(sys.stderr, color=True, bold=True, italic=True)
|
||||||
|
|
||||||
|
def dye_out(s, color='end'):
|
||||||
|
return '{}{}{}'.format(COLORS_OUT[color], s, COLORS_OUT['end'])
|
||||||
|
|
||||||
|
def dye_err(s, color='end'):
|
||||||
|
return '{}{}{}'.format(COLORS_ERR[color], s, COLORS_OUT['end'])
|
||||||
|
|
||||||
_dye = dye
|
|
||||||
def _nodye(s, *args, **kwargs):
|
def _nodye(s, *args, **kwargs):
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def setup(enable = True):
|
def setup(color=True, bold=True, italic=True):
|
||||||
global dye
|
global COLORS_OUT, COLORS_ERR
|
||||||
if enable:
|
COLORS_OUT = generate_colors(sys.stdout, color=color, bold=color, italic=color)
|
||||||
dye = _dye
|
COLORS_ERR = generate_colors(sys.stderr, color=color, bold=color, italic=color)
|
||||||
else:
|
|
||||||
dye = _nodye
|
|
||||||
|
|
||||||
|
|
||||||
|
# undye
|
||||||
undye_re = re.compile('\x1b\[[;\d]*[A-Za-z]')
|
undye_re = re.compile('\x1b\[[;\d]*[A-Za-z]')
|
||||||
|
|
||||||
def undye(s):
|
def undye(s):
|
||||||
"""Purge string s of color"""
|
"""Purge string s of color"""
|
||||||
return undye_re.sub('', s)
|
return undye_re.sub('', s)
|
||||||
|
|
||||||
|
# colors
|
||||||
|
ok = 'green'
|
||||||
|
error = 'red'
|
||||||
|
citekey = 'purple'
|
||||||
|
filepath = 'bold'
|
||||||
|
tag = 'cyan'
|
||||||
|
@ -128,13 +128,14 @@ def command(args):
|
|||||||
try:
|
try:
|
||||||
rp.push_paper(p)
|
rp.push_paper(p)
|
||||||
if docfile is not None:
|
if docfile is not None:
|
||||||
rp.push_doc(p.citekey, docfile, copy=args.copy)
|
rp.push_doc(p.citekey, docfile, copy=args.copy or args.move)
|
||||||
if args.copy:
|
if args.copy:
|
||||||
if args.move:
|
if args.move:
|
||||||
content.remove_file(docfile)
|
content.remove_file(docfile)
|
||||||
elif ui.input_yn('{} has been copied into pubs; should the original be removed?'.format(color.dye(docfile, color.bold))):
|
# elif ui.input_yn('{} has been copied into pubs; should the original be removed?'.format(color.dye_out(docfile, 'bold'))):
|
||||||
content.remove_file(docfile)
|
# content.remove_file(docfile)
|
||||||
ui.print_('added to pubs:\n{}'.format(pretty.paper_oneliner(p)))
|
|
||||||
|
ui.print_out('added to pubs:\n{}'.format(pretty.paper_oneliner(p)))
|
||||||
except ValueError as v:
|
except ValueError as v:
|
||||||
ui.error(v.message)
|
ui.error(v.message)
|
||||||
ui.exit(1)
|
ui.exit(1)
|
||||||
|
@ -34,10 +34,14 @@ def command(args):
|
|||||||
try:
|
try:
|
||||||
document = args.document
|
document = args.document
|
||||||
rp.push_doc(paper.citekey, document, copy=args.copy)
|
rp.push_doc(paper.citekey, document, copy=args.copy)
|
||||||
if args.copy and args.move:
|
if args.copy:
|
||||||
|
if args.move:
|
||||||
content.remove_file(document)
|
content.remove_file(document)
|
||||||
|
# else:
|
||||||
|
# if ui.input_yn('{} has been copied into pubs; should the original be removed?'.format(color.dye_out(document, 'bold'))):
|
||||||
|
# content.remove_file(document)
|
||||||
|
|
||||||
ui.print_('{} attached to {}'.format(color.dye(document, color.bold), color.dye(paper.citekey, color.citekey)))
|
ui.print_out('{} attached to {}'.format(color.dye_out(document, 'bold'), color.dye_out(paper.citekey, color.citekey)))
|
||||||
|
|
||||||
except ValueError as v:
|
except ValueError as v:
|
||||||
ui.error(v.message)
|
ui.error(v.message)
|
||||||
|
@ -36,4 +36,4 @@ def command(args):
|
|||||||
bib[p.citekey] = p.bibdata
|
bib[p.citekey] = p.bibdata
|
||||||
exporter = endecoder.EnDecoder()
|
exporter = endecoder.EnDecoder()
|
||||||
bibdata_raw = exporter.encode_bibdata(bib)
|
bibdata_raw = exporter.encode_bibdata(bib)
|
||||||
ui.print_(bibdata_raw)
|
ui.print_out(bibdata_raw)
|
||||||
|
@ -80,7 +80,7 @@ def command(args):
|
|||||||
ui.error('could not load entry for citekey {}.'.format(k))
|
ui.error('could not load entry for citekey {}.'.format(k))
|
||||||
else:
|
else:
|
||||||
rp.push_paper(p)
|
rp.push_paper(p)
|
||||||
ui.print_('{} imported'.format(color.dye(p.citekey, color.cyan)))
|
ui.print_out('{} imported'.format(color.dye_out(p.citekey, color.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))
|
||||||
|
@ -34,11 +34,10 @@ def command(args):
|
|||||||
|
|
||||||
if check_directory(pubsdir, fail=False) and len(os.listdir(pubsdir)) > 0:
|
if check_directory(pubsdir, fail=False) and len(os.listdir(pubsdir)) > 0:
|
||||||
ui.error('directory {} is not empty.'.format(
|
ui.error('directory {} is not empty.'.format(
|
||||||
color.dye(pubsdir, color.filepath)))
|
color.dye_err(pubsdir, color.filepath)))
|
||||||
ui.exit()
|
ui.exit()
|
||||||
|
|
||||||
ui.print_('Initializing pubs in {}'.format(
|
ui.print_out('Initializing pubs in {}'.format(color.dye_out(pubsdir, color.filepath)))
|
||||||
color.dye(pubsdir, color.filepath)))
|
|
||||||
|
|
||||||
config().pubsdir = pubsdir
|
config().pubsdir = pubsdir
|
||||||
config().docsdir = docsdir
|
config().docsdir = docsdir
|
||||||
|
@ -49,7 +49,7 @@ def command(args):
|
|||||||
else:
|
else:
|
||||||
papers = sorted(papers, key=date_added)
|
papers = sorted(papers, key=date_added)
|
||||||
if len(papers) > 0:
|
if len(papers) > 0:
|
||||||
ui.print_('\n'.join(
|
ui.print_out('\n'.join(
|
||||||
pretty.paper_oneliner(p, citekey_only=args.citekeys)
|
pretty.paper_oneliner(p, citekey_only=args.citekeys)
|
||||||
for p in papers))
|
for p in papers))
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ def command(args):
|
|||||||
|
|
||||||
if paper.docpath is None:
|
if paper.docpath is None:
|
||||||
ui.error('No document associated with the entry {}.'.format(
|
ui.error('No document associated with the entry {}.'.format(
|
||||||
color.dye(citekey, color.citekey)))
|
color.dye_err(citekey, color.citekey)))
|
||||||
ui.exit()
|
ui.exit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -39,7 +39,7 @@ def command(args):
|
|||||||
cmd = with_command.split()
|
cmd = with_command.split()
|
||||||
cmd.append(docpath)
|
cmd.append(docpath)
|
||||||
subprocess.Popen(cmd)
|
subprocess.Popen(cmd)
|
||||||
ui.print_('{} opened.'.format(color.dye(docpath, color.filepath)))
|
ui.print_out('{} opened.'.format(color.dye(docpath, color.filepath)))
|
||||||
except OSError:
|
except OSError:
|
||||||
ui.error("Command does not exist: %s." % with_command)
|
ui.error("Command does not exist: %s." % with_command)
|
||||||
ui.exit(127)
|
ui.exit(127)
|
||||||
|
@ -22,12 +22,12 @@ def command(args):
|
|||||||
if force is None:
|
if force is None:
|
||||||
are_you_sure = (("Are you sure you want to delete paper(s) [{}]"
|
are_you_sure = (("Are you sure you want to delete paper(s) [{}]"
|
||||||
" (this will also delete associated documents)?")
|
" (this will also delete associated documents)?")
|
||||||
.format(', '.join([color.dye(c, color.citekey) for c in args.citekeys])))
|
.format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys])))
|
||||||
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 args.citekeys:
|
for c in args.citekeys:
|
||||||
rp.remove_paper(c)
|
rp.remove_paper(c)
|
||||||
ui.print_('The paper(s) [{}] were removed'.format(', '.join([color.dye(c, color.citekey) for c in args.citekeys])))
|
ui.print_out('The paper(s) [{}] were removed'.format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys])))
|
||||||
# FIXME: print should check that removal proceeded well.
|
# FIXME: print should check that removal proceeded well.
|
||||||
else:
|
else:
|
||||||
ui.print_('The paper(s) [{}] were not removed'.format(', '.join([color.dye(c, color.citekey) for c in args.citekeys])))
|
ui.print_out('The paper(s) [{}] were *not* removed'.format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys])))
|
||||||
|
@ -83,13 +83,13 @@ def command(args):
|
|||||||
rp = Repository(config())
|
rp = Repository(config())
|
||||||
|
|
||||||
if citekeyOrTag is None:
|
if citekeyOrTag is None:
|
||||||
ui.print_(color.dye(' '.join(sorted(rp.get_tags())), color=color.blue))
|
ui.print_out(color.dye_out(' '.join(sorted(rp.get_tags())), color.tag))
|
||||||
else:
|
else:
|
||||||
if rp.databroker.exists(citekeyOrTag):
|
if rp.databroker.exists(citekeyOrTag):
|
||||||
p = rp.pull_paper(citekeyOrTag)
|
p = rp.pull_paper(citekeyOrTag)
|
||||||
if tags == []:
|
if tags == []:
|
||||||
ui.print_(color.dye(' '.join(sorted(p.tags)),
|
ui.print_out(color.dye_out(' '.join(sorted(p.tags)),
|
||||||
color=color.blue))
|
color.tag))
|
||||||
else:
|
else:
|
||||||
add_tags, remove_tags = _tag_groups(_parse_tags(tags))
|
add_tags, remove_tags = _tag_groups(_parse_tags(tags))
|
||||||
for tag in add_tags:
|
for tag in add_tags:
|
||||||
@ -108,5 +108,5 @@ def command(args):
|
|||||||
len(p.tags.intersection(excluded)) == 0):
|
len(p.tags.intersection(excluded)) == 0):
|
||||||
papers_list.append(p)
|
papers_list.append(p)
|
||||||
|
|
||||||
ui.print_('\n'.join(pretty.paper_oneliner(p)
|
ui.print_out('\n'.join(pretty.paper_oneliner(p)
|
||||||
for p in papers_list))
|
for p in papers_list))
|
||||||
|
@ -19,15 +19,15 @@ def command(args):
|
|||||||
repo_version = int(config().version)
|
repo_version = int(config().version)
|
||||||
|
|
||||||
if repo_version == code_version:
|
if repo_version == code_version:
|
||||||
ui.print_('Your pubs repository is up-to-date.')
|
ui.print_out('Your pubs repository is up-to-date.')
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif repo_version > code_version:
|
elif repo_version > code_version:
|
||||||
ui.print_('Your repository was generated with an newer version of pubs.\n'
|
ui.print_out('Your repository was generated with an newer version of pubs.\n'
|
||||||
'You should not use pubs until you install the newest version.')
|
'You should not use pubs until you install the newest version.')
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
msg = ("You should backup the pubs directory {} before continuing."
|
msg = ("You should backup the pubs directory {} before continuing."
|
||||||
"Continue ?").format(color.dye(config().papers_dir, color.filepath))
|
"Continue ?").format(color.dye_out(config().papers_dir, color.filepath))
|
||||||
sure = ui.input_yn(question=msg, default='n')
|
sure = ui.input_yn(question=msg, default='n')
|
||||||
if not sure:
|
if not sure:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
@ -102,7 +102,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.print_(u'dowloading {}'.format(path))
|
ui.print_out(u'dowloading {}'.format(path))
|
||||||
response = urlopen(path)
|
response = urlopen(path)
|
||||||
return response.read()
|
return response.read()
|
||||||
|
|
||||||
|
13
pubs/p3.py
13
pubs/p3.py
@ -13,6 +13,8 @@ if sys.version_info[0] == 2:
|
|||||||
# for test_usecase.
|
# for test_usecase.
|
||||||
def _get_raw_stdout():
|
def _get_raw_stdout():
|
||||||
return sys.stdout
|
return sys.stdout
|
||||||
|
def _get_raw_stderr():
|
||||||
|
return sys.stderr
|
||||||
|
|
||||||
ustr = unicode
|
ustr = unicode
|
||||||
uchr = unichr
|
uchr = unichr
|
||||||
@ -37,6 +39,13 @@ else:
|
|||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
from http.client import HTTPConnection
|
from http.client import HTTPConnection
|
||||||
|
|
||||||
|
# The following has to be a function so that it can be mocked
|
||||||
|
# for test_usecase.
|
||||||
|
def _get_raw_stdout():
|
||||||
|
return sys.stdout.buffer
|
||||||
|
def _get_raw_stderr():
|
||||||
|
return sys.stderr.buffer
|
||||||
|
|
||||||
def _fake_stdio():
|
def _fake_stdio():
|
||||||
return io.TextIOWrapper(io.BytesIO()) # Only for tests to capture std{out,err}
|
return io.TextIOWrapper(io.BytesIO()) # Only for tests to capture std{out,err}
|
||||||
|
|
||||||
@ -45,10 +54,6 @@ else:
|
|||||||
stdio.seek(0)
|
stdio.seek(0)
|
||||||
return stdio.read()
|
return stdio.read()
|
||||||
|
|
||||||
# The following has to be a function so that it can be mocked
|
|
||||||
# for test_usecase.
|
|
||||||
def _get_raw_stdout():
|
|
||||||
return sys.stdout.buffer
|
|
||||||
|
|
||||||
configparser = configparser
|
configparser = configparser
|
||||||
input = input
|
input = input
|
||||||
|
@ -33,9 +33,9 @@ def bib_oneliner(bibdata):
|
|||||||
journal = ' ' + bibdata.get('booktitle', '')
|
journal = ' ' + bibdata.get('booktitle', '')
|
||||||
|
|
||||||
return u'{authors} \"{title}\"{journal}{year}'.format(
|
return u'{authors} \"{title}\"{journal}{year}'.format(
|
||||||
authors=color.dye(authors, color.grey, bold=True),
|
authors=color.dye_out(authors, 'bold'),
|
||||||
title=bibdata.get('title', ''),
|
title=bibdata.get('title', ''),
|
||||||
journal=color.dye(journal, color.yellow),
|
journal=color.dye_out(journal, 'italic'),
|
||||||
year=' ({})'.format(bibdata['year']) if 'year' in bibdata else '',
|
year=' ({})'.format(bibdata['year']) if 'year' in bibdata else '',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ def paper_oneliner(p, citekey_only=False):
|
|||||||
else:
|
else:
|
||||||
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(t, color.tag) for t in sorted(p.tags)))
|
','.join(color.dye_out(t, color.tag) for t in sorted(p.tags)))
|
||||||
return u'[{citekey}] {descr} {tags}'.format(
|
return u'[{citekey}] {descr} {tags}'.format(
|
||||||
citekey=color.dye(p.citekey, color.purple),
|
citekey=color.dye_out(p.citekey, 'purple'),
|
||||||
descr=bibdesc, tags=tags)
|
descr=bibdesc, tags=tags)
|
||||||
|
@ -46,7 +46,7 @@ def _update_check(config, ui):
|
|||||||
'to bypass this error)')
|
'to bypass this error)')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
elif repo_version < code_version:
|
elif repo_version < code_version:
|
||||||
ui.print_(
|
ui.print_out(
|
||||||
'warning: your repository version (v{})'.format(repo_version)
|
'warning: your repository version (v{})'.format(repo_version)
|
||||||
+ 'must be updated to version {}.\n'.format(code_version)
|
+ 'must be updated to version {}.\n'.format(code_version)
|
||||||
+ "run 'pubs update'.")
|
+ "run 'pubs update'.")
|
||||||
|
70
pubs/uis.py
70
pubs/uis.py
@ -6,7 +6,7 @@ import codecs
|
|||||||
|
|
||||||
from .content import editor_input
|
from .content import editor_input
|
||||||
from . import color
|
from . import color
|
||||||
from .p3 import _get_raw_stdout, input
|
from .p3 import _get_raw_stdout, _get_raw_stderr, input
|
||||||
|
|
||||||
|
|
||||||
# package-shared ui that can be accessed using :
|
# package-shared ui that can be accessed using :
|
||||||
@ -23,7 +23,7 @@ def _get_encoding(config):
|
|||||||
enc = locale.getdefaultlocale()[1]
|
enc = locale.getdefaultlocale()[1]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass # Keep default
|
pass # Keep default
|
||||||
return config.get('terminal-encoding', enc or 'utf8')
|
return config.get('terminal-encoding', enc or 'utf-8')
|
||||||
|
|
||||||
|
|
||||||
def get_ui():
|
def get_ui():
|
||||||
@ -34,34 +34,60 @@ def get_ui():
|
|||||||
|
|
||||||
def init_ui(config):
|
def init_ui(config):
|
||||||
global _ui
|
global _ui
|
||||||
_ui = UI(config)
|
_ui = InputUI(config)
|
||||||
|
|
||||||
|
|
||||||
class UI:
|
class PrintUI(object):
|
||||||
"""UI class. Stores configuration parameters and system information.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
color.setup(config.color)
|
color.setup(config.color)
|
||||||
self.editor = config.edit_cmd
|
|
||||||
self.encoding = _get_encoding(config)
|
self.encoding = _get_encoding(config)
|
||||||
self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(),
|
self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(),
|
||||||
errors='replace')
|
errors='replace')
|
||||||
|
self._stderr = codecs.getwriter(self.encoding)(_get_raw_stderr(),
|
||||||
|
errors='replace')
|
||||||
|
|
||||||
def print_(self, *strings, **kwargs):
|
def print_out(self, *strings, **kwargs):
|
||||||
"""Like print, but rather than raising an error when a character
|
"""Like print, but rather than raising an error when a character
|
||||||
is not in the terminal's encoding's character set, just silently
|
is not in the terminal's encoding's character set, just silently
|
||||||
replaces it.
|
replaces it.
|
||||||
"""
|
"""
|
||||||
print(' '.join(strings), file=self._stdout, **kwargs)
|
print(' '.join(strings), file=self._stdout, **kwargs)
|
||||||
|
|
||||||
|
def print_err(self, *strings, **kwargs):
|
||||||
|
"""Like print, but rather than raising an error when a character
|
||||||
|
is not in the terminal's encoding's character set, just silently
|
||||||
|
replaces it.
|
||||||
|
"""
|
||||||
|
print(' '.join(strings), file=self._stderr, **kwargs)
|
||||||
|
|
||||||
|
def error(self, message):
|
||||||
|
self.print_err('{}: {}'.format(color.dye_err('error', 'red'), message))
|
||||||
|
|
||||||
|
|
||||||
|
def warning(self, message):
|
||||||
|
self.print_err("%s: %s" % (color.dye_err('warning', 'yellow'), message))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InputUI(PrintUI):
|
||||||
|
"""UI class. Stores configuration parameters and system information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
super(InputUI, self).__init__(config)
|
||||||
|
self.editor = config.edit_cmd
|
||||||
|
|
||||||
|
def exit(self, error_code=1):
|
||||||
|
sys.exit(error_code)
|
||||||
|
|
||||||
def input(self):
|
def input(self):
|
||||||
try:
|
try:
|
||||||
data = input()
|
data = input()
|
||||||
except EOFError:
|
except EOFError:
|
||||||
self.error('Standard input ended while waiting for answer.')
|
self.error(u'Standard input ended while waiting for answer.')
|
||||||
self.exit(1)
|
self.exit(1)
|
||||||
return data
|
return data.decode('utf-8')
|
||||||
|
|
||||||
def input_choice_ng(self, options, option_chars=None, default=None, question=''):
|
def input_choice_ng(self, options, option_chars=None, default=None, question=''):
|
||||||
"""Ask the user to chose between a set of options. The iser is asked
|
"""Ask the user to chose between a set of options. The iser is asked
|
||||||
@ -75,7 +101,7 @@ class UI:
|
|||||||
:returns: int
|
:returns: int
|
||||||
the index of the chosen option
|
the index of the chosen option
|
||||||
"""
|
"""
|
||||||
char_color = color.bold
|
char_color = 'bold'
|
||||||
option_chars = [s[0] for s in options]
|
option_chars = [s[0] for s in options]
|
||||||
displayed_chars = [c.upper() if i == default else c
|
displayed_chars = [c.upper() if i == default else c
|
||||||
for i, c in enumerate(option_chars)]
|
for i, c in enumerate(option_chars)]
|
||||||
@ -84,10 +110,10 @@ class UI:
|
|||||||
option_chars = []
|
option_chars = []
|
||||||
char_color = color.end
|
char_color = color.end
|
||||||
|
|
||||||
option_str = '/'.join(["{}{}".format(color.dye(c, color.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.print_('{} {}: '.format(question, option_str), end='')
|
self.print_out('{} {}: '.format(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 == '':
|
||||||
@ -101,8 +127,7 @@ class UI:
|
|||||||
return option_chars.index(answer.lower())
|
return option_chars.index(answer.lower())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
self.print_('Incorrect option.', option_str)
|
self.print_out('Incorrect option.', option_str)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def input_choice(self, options, option_chars, default=None, question=''):
|
def input_choice(self, options, option_chars, default=None, question=''):
|
||||||
@ -122,9 +147,9 @@ class UI:
|
|||||||
"""
|
"""
|
||||||
displayed_chars = [s.upper() if i == default else s
|
displayed_chars = [s.upper() if i == default else s
|
||||||
for i, s in enumerate(option_chars)]
|
for i, s in enumerate(option_chars)]
|
||||||
option_str = ', '.join(["[%s]%s" % (color.dye(c, color.cyan), o)
|
option_str = ', '.join(["[%s]%s" % (color.dye_out(c, 'cyan'), o)
|
||||||
for c, o in zip(displayed_chars, options)])
|
for c, o in zip(displayed_chars, options)])
|
||||||
self.print_(question, option_str)
|
self.print_out(question, option_str)
|
||||||
while True:
|
while True:
|
||||||
answer = self.input()
|
answer = self.input()
|
||||||
if answer is None or answer == '':
|
if answer is None or answer == '':
|
||||||
@ -135,21 +160,12 @@ class UI:
|
|||||||
return option_chars.index(answer.lower())
|
return option_chars.index(answer.lower())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
self.print_('Incorrect option.', option_str)
|
self.print_out('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
|
||||||
answer = self.input_choice_ng(['yes', 'no'], default=d, question=question)
|
answer = self.input_choice_ng(['yes', 'no'], default=d, question=question)
|
||||||
return [True, False][answer]
|
return [True, False][answer]
|
||||||
|
|
||||||
def exit(self, error_code=1):
|
|
||||||
sys.exit(error_code)
|
|
||||||
|
|
||||||
def error(self, message):
|
|
||||||
self.print_("%s: %s" % (color.dye('error', color.red), message))
|
|
||||||
|
|
||||||
def warning(self, message):
|
|
||||||
self.print_("%s: %s" % (color.dye('warning', color.yellow), message))
|
|
||||||
|
|
||||||
def editor_input(self, initial="", suffix='.tmp'):
|
def editor_input(self, initial="", suffix='.tmp'):
|
||||||
return editor_input(self.editor, initial=initial, suffix=suffix)
|
return editor_input(self.editor, initial=initial, suffix=suffix)
|
||||||
|
@ -24,9 +24,7 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True):
|
|||||||
citekey))
|
citekey))
|
||||||
for c in citekeys:
|
for c in citekeys:
|
||||||
p = repo.pull_paper(c)
|
p = repo.pull_paper(c)
|
||||||
ui.print_(u' {}'.format(pretty.paper_oneliner(p)))
|
ui.print_out(u' {}'.format(pretty.paper_oneliner(p)))
|
||||||
if exit_on_fail:
|
if exit_on_fail:
|
||||||
ui.exit()
|
ui.exit()
|
||||||
return citekey
|
return citekey
|
||||||
|
|
||||||
|
|
||||||
|
3
setup.py
3
setup.py
@ -11,7 +11,8 @@ setup(
|
|||||||
url = '',
|
url = '',
|
||||||
|
|
||||||
description = 'command-line scientific bibliography manager',
|
description = 'command-line scientific bibliography manager',
|
||||||
packages = find_packages(), #['pubs', 'pubs.commands', 'pubs.templates', 'pubs.plugs'],
|
#packages = find_packages(), #['pubs', 'pubs.commands', 'pubs.templates', 'pubs.plugs'],
|
||||||
|
packages = ['pubs', 'pubs.commands', 'pubs.templates', 'pubs.plugs'],
|
||||||
scripts = ['pubs/pubs'],
|
scripts = ['pubs/pubs'],
|
||||||
|
|
||||||
install_requires = ['pyyaml', 'bibtexparser', 'python-dateutil', 'requests'],
|
install_requires = ['pyyaml', 'bibtexparser', 'python-dateutil', 'requests'],
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
# Adjusting paths.
|
# Adjusting paths.
|
||||||
import os, sys
|
import os, sys
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../..')))
|
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../..')))
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
@ -11,14 +11,16 @@ coder = endecoder.EnDecoder()
|
|||||||
franny_bib = """@article{Franny1961,
|
franny_bib = """@article{Franny1961,
|
||||||
author = "Salinger, J. D.",
|
author = "Salinger, J. D.",
|
||||||
title = "Franny and Zooey",
|
title = "Franny and Zooey",
|
||||||
year = "1961"}
|
year = "1961",
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
doe_bib = """
|
doe_bib = """
|
||||||
@article{Doe2013,
|
@article{Doe2013,
|
||||||
author = "Doe, John",
|
author = "Doe, John",
|
||||||
title = "Nice Title",
|
title = "Nice Title",
|
||||||
year = "2013"}
|
year = "2013",
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dummy_metadata = {'docfile': 'docsdir://hop.la', 'tags': set(['a', 'b'])}
|
dummy_metadata = {'docfile': 'docsdir://hop.la', 'tags': set(['a', 'b'])}
|
||||||
|
@ -29,7 +29,7 @@ bibtex_raw0 = """@techreport{
|
|||||||
note = "Previous number = SIDL-WP-1999-0120",
|
note = "Previous number = SIDL-WP-1999-0120",
|
||||||
year = "1999",
|
year = "1999",
|
||||||
type = "Technical Report",
|
type = "Technical Report",
|
||||||
institution = "Stanford InfoLab"
|
institution = "Stanford InfoLab",
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -13,7 +13,7 @@ from str_fixtures import bibtex_raw0
|
|||||||
class TestPretty(unittest.TestCase):
|
class TestPretty(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
color.setup(enable=False)
|
color.setup()
|
||||||
|
|
||||||
def test_oneliner(self):
|
def test_oneliner(self):
|
||||||
decoder = endecoder.EnDecoder()
|
decoder = endecoder.EnDecoder()
|
||||||
|
@ -53,7 +53,7 @@ class TestFakeInput(unittest.TestCase):
|
|||||||
class CommandTestCase(unittest.TestCase):
|
class CommandTestCase(unittest.TestCase):
|
||||||
"""Abstract TestCase intializing the fake filesystem."""
|
"""Abstract TestCase intializing the fake filesystem."""
|
||||||
|
|
||||||
maxDiff = None
|
maxDiff = 1000000
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.fs = fake_env.create_fake_fs([content, filebroker, configs, init_cmd, import_cmd])
|
self.fs = fake_env.create_fake_fs([content, filebroker, configs, init_cmd, import_cmd])
|
||||||
@ -62,42 +62,43 @@ class CommandTestCase(unittest.TestCase):
|
|||||||
def execute_cmds(self, cmds, capture_output=CAPTURE_OUTPUT):
|
def execute_cmds(self, cmds, capture_output=CAPTURE_OUTPUT):
|
||||||
""" Execute a list of commands, and capture their output
|
""" Execute a list of commands, and capture their output
|
||||||
|
|
||||||
A command can be a string, or a tuple of size 2 or 3.
|
A command can be a string, or a tuple of size 2, 3 or 4.
|
||||||
In the latter case, the command is :
|
In the latter case, the command is :
|
||||||
1. a string reprensenting the command to execute
|
1. a string reprensenting the command to execute
|
||||||
2. the user inputs to feed to the command during execution
|
2. the user inputs to feed to the command during execution
|
||||||
3. the output expected, verified with assertEqual. Always captures
|
3. the expected output on stdout, verified with assertEqual.
|
||||||
output in this case.
|
4. the expected output on stderr, verified with assertEqual.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
outs = []
|
outs = []
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
inputs = []
|
inputs = []
|
||||||
output = None
|
expected_out, expected_err = None, None
|
||||||
actual_cmd = cmd
|
actual_cmd = cmd
|
||||||
current_capture_output = capture_output
|
|
||||||
if not isinstance(cmd, p3.ustr):
|
if not isinstance(cmd, p3.ustr):
|
||||||
actual_cmd = cmd[0]
|
actual_cmd = cmd[0]
|
||||||
if len(cmd) == 2: # Inputs provided
|
if len(cmd) == 2: # Inputs provided
|
||||||
inputs = cmd[1]
|
inputs = cmd[1]
|
||||||
if len(cmd) == 3: # Expected output provided
|
if len(cmd) == 3: # Expected output provided
|
||||||
current_capture_output = True
|
capture_output = True
|
||||||
output = cmd[2]
|
expected_out = color.undye(cmd[2])
|
||||||
|
if len(cmd) == 4: # Expected error output provided
|
||||||
|
expected_err = color.undye(cmd[3])
|
||||||
# Always set fake input: test should not ask unexpected user input
|
# Always set fake input: test should not ask unexpected user input
|
||||||
input = fake_env.FakeInput(inputs, [content, uis, p3])
|
input = fake_env.FakeInput(inputs, [content, uis, p3])
|
||||||
input.as_global()
|
input.as_global()
|
||||||
try:
|
try:
|
||||||
if current_capture_output:
|
if capture_output:
|
||||||
_, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(
|
_, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(
|
||||||
actual_cmd.split())
|
actual_cmd.split())
|
||||||
self.assertEqual(stderr, '')
|
|
||||||
actual_out = color.undye(stdout)
|
actual_out = color.undye(stdout)
|
||||||
if output is not None:
|
actual_err = color.undye(stderr)
|
||||||
correct_out = color.undye(output)
|
if expected_out is not None:
|
||||||
self.assertEqual(actual_out, correct_out)
|
self.assertEqual(actual_out, expected_out)
|
||||||
|
if expected_err is not None:
|
||||||
|
self.assertEqual(actual_err, expected_err)
|
||||||
outs.append(color.undye(actual_out))
|
outs.append(color.undye(actual_out))
|
||||||
else:
|
else:
|
||||||
pubs_cmd.execute(cmd.split())
|
pubs_cmd.execute(actual_cmd.split())
|
||||||
except fake_env.FakeInput.UnexpectedInput:
|
except fake_env.FakeInput.UnexpectedInput:
|
||||||
self.fail('Unexpected input asked by command: {}.'.format(
|
self.fail('Unexpected input asked by command: {}.'.format(
|
||||||
actual_cmd))
|
actual_cmd))
|
||||||
@ -252,7 +253,7 @@ class TestUsecase(DataCommandTestCase):
|
|||||||
|
|
||||||
def test_first(self):
|
def test_first(self):
|
||||||
correct = ['Initializing pubs in /paper_first\n',
|
correct = ['Initializing pubs in /paper_first\n',
|
||||||
'[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \nwas added to pubs.\n',
|
'added to pubs:\n[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n',
|
||||||
'[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n',
|
'[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n',
|
||||||
'\n',
|
'\n',
|
||||||
'',
|
'',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user