Merge branch 'main' of github.com:pubs/pubs

main
Jonas Kulhanek 3 years ago
commit dfc332006e

@ -0,0 +1,60 @@
name: Pubs tests
on:
push:
pull_request:
schedule:
- cron: '0 8 * * *'
jobs:
unit-test:
name: Run unit tests
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
python-version: [3.6, 3.7, 3.8, 3.9]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r dev_requirements.txt
- name: Configure git author (fix issue with environment variable)
run: |
# Manually sets some git user and email to avoid failure of the test
# (For some reason the environment variables set in the test are not
# taken into account by git on the runner.)
git config --global user.name "Pubs test"
git config --global user.email "unittest@pubs.org"
- name: Test with pytest (mock API mode)
env:
PUBS_TESTS_MODE: MOCK
run: pytest
- name: Test with pytest (online API mode)
env:
PUBS_TESTS_MODE: COLLECT
run: pytest
install-test:
name: Test installation
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
if: github.event_name == 'schedule'
steps:
- uses: actions/setup-python@v2
- name: install test
run: |
pip install -U pip
pip install pubs
pubs --help
pip uninstall -y pubs

@ -1,105 +0,0 @@
# list of environments to test
matrix:
include:
# Full tests (with online API)
- os: linux
language: python
python: 2.7
env:
- TO_TEST=TEST_FULL
- os: linux
language: python
python: 3.7
dist: xenial
sudo: true
env:
- TO_TEST=TEST_FULL
- os: osx
language: generic
python: 2.7
env:
- TO_TEST=TEST_FULL
# before_install:
# - python2 --version
# - pip2 install -U virtualenv
# - virtualenv env -p python2
# - source env/bin/activate
- os: osx
language: generic
python: ">=3.6"
env:
- TO_TEST=TEST_FULL
before_install:
- brew outdated python3 || brew install python3 || brew upgrade python3
- python3 -m venv env
- source env/bin/activate
# Mock tests (with mock API)
- os: linux
language: python
python: 3.3
env:
- TO_TEST=TEST_MOCK
- os: linux
language: python
python: 3.4
env:
- TO_TEST=TEST_MOCK
- os: linux
language: python
python: 3.5
env:
- TO_TEST=TEST_MOCK
- os: linux
language: python
python: 3.6
env:
- TO_TEST=TEST_MOCK
- os: linux
language: python
dist: xenial
python: 3.7
sudo: true
env:
- TO_TEST=TEST_MOCK
# Install tests
- os: linux
language: python
python: 2.7
env:
- TO_TEST=INSTALL
if: type = cron
- os: linux
language: python
dist: xenial
sudo: true
python: 3.7
env:
- TO_TEST=INSTALL
if: type = cron
- os: osx
language: generic
python: 2.7
env:
- TO_TEST=INSTALL
if: type = cron
- os: osx
language: generic
python: ">=3.6"
env:
- TO_TEST=INSTALL
if: type = cron
allow_failures:
- python: 3.3
# command to run tests
script:
- python --version
- if [ "$TO_TEST" = "TEST_MOCK" ] ||
[ "$TO_TEST" = "TEST_FULL" ]; then PUBS_TESTS_MODE=MOCK python setup.py test; fi
- if [ "$TO_TEST" = "TEST_FULL" ]; then PUBS_TESTS_MODE=COLLECT python setup.py test; fi
- if [ "$TO_TEST" = "INSTALL" ]; then pip install -U pip && pip install pubs && pubs --help && pip uninstall -y pubs; fi

@ -5,8 +5,24 @@
[Full Changelog](https://github.com/pubs/pubs/compare/v0.8.3...master) [Full Changelog](https://github.com/pubs/pubs/compare/v0.8.3...master)
### Implemented enhancements
- Migration from Travis CI to Github actions ([#260](https://github.com/pubs/pubs/pull/260))
- Allow passing named arguments to custom commands ([#241](https://github.com/pubs/pubs/pull/241) by [jkulhanek](https://github.com/jkulhanek))
- Added support for non-standard bibtex types, e.g. @collection, @software, etc. ([#226](https://github.com/pubs/pubs/pull/226))
- The number of displayed authors in listings is now configurable, as the `max_authors` value in the `main` section of the configuration. ([#225](https://github.com/pubs/pubs/pull/225))
- More explicit add command dialogs when copying and moving documents.
- Empty tags are not added to papers anymore.
### Fixed bugs ### Fixed bugs
- Fixed collision when entry uses `type` field ([#252](https://github.com/pubs/pubs/pull/252))
- Note on comma in alias descriptions ([#240](https://github.com/pubs/pubs/pull/240) [StanczakDominik](https://github.com/StanczakDominik))
- Note path correctly expand user '~' ([#250](https://github.com/pubs/pubs/pull/250))
- Tests don't run on python 2.7 or <=3.4. They may still work, but support will not be tested and will eventually be dropped. ([#223](https://github.com/pubs/pubs/pull/223))
- Fixed the reported number of paper with a tag in the statistic command ([#232](https://github.com/pubs/pubs/pull/232) by [beuerle](https://github.com/beuerle))
- Fixed a crash when resolving citekeys introduced by [#225](https://github.com/pubs/pubs/pull/225) ([#233](https://github.com/pubs/pubs/pull/233) by [beuerle](https://github.com/beuerle))
## [v0.8.3](https://github.com/pubs/pubs/compare/v0.8.2...v0.8.3) (2019-08-12) ## [v0.8.3](https://github.com/pubs/pubs/compare/v0.8.2...v0.8.3) (2019-08-12)
@ -38,7 +54,6 @@ A hotfix release. All users of 0.8.0 are urged to upgrade.
### Fixed bugs ### Fixed bugs
- Fix adding paper with DOIs, ISBNs or arXiv references. [(#165)](https://github.com/pubs/pubs/pull/165) - Fix adding paper with DOIs, ISBNs or arXiv references. [(#165)](https://github.com/pubs/pubs/pull/165)
- Fix statistics command when there is not yet any paper in the repository. [(#164)](https://github.com/pubs/pubs/pull/164) - Fix statistics command when there is not yet any paper in the repository. [(#164)](https://github.com/pubs/pubs/pull/164)
@ -49,69 +64,39 @@ A long overdue feature release. Add supports for arXiv bibtex fetching, and many
### Implemented enhancements ### Implemented enhancements
- Adds `move`, and `link` options for handling of documents during `import` (copy being the default). Makes `copy` the default for document handling during `add`. [(#159)](https://github.com/pubs/pubs/pull/159) - Adds `move`, and `link` options for handling of documents during `import` (copy being the default). Makes `copy` the default for document handling during `add`. [(#159)](https://github.com/pubs/pubs/pull/159)
- Support for downloading arXiv reference from their ID ([#146](https://github.com/pubs/pubs/issues/146) by [joe-antognini](https://github.com/joe-antognini)) - Support for downloading arXiv reference from their ID ([#146](https://github.com/pubs/pubs/issues/146) by [joe-antognini](https://github.com/joe-antognini))
- Better feedback when an error is encountered while adding a reference from a DOI, ISBN or arXiv ID [#155](https://github.com/pubs/pubs/issues/155) - Better feedback when an error is encountered while adding a reference from a DOI, ISBN or arXiv ID [#155](https://github.com/pubs/pubs/issues/155)
- Better dialog after editing paper [(#142)](https://github.com/pubs/pubs/issues/142) - Better dialog after editing paper [(#142)](https://github.com/pubs/pubs/issues/142)
- Add a command to open urls ([#139](https://github.com/pubs/pubs/issues/139) by [ksunden](https://github.com/ksunden)) - Add a command to open urls ([#139](https://github.com/pubs/pubs/issues/139) by [ksunden](https://github.com/ksunden))
- More robust cache on version change [(#138)](https://github.com/pubs/pubs/issues/138) - More robust cache on version change [(#138)](https://github.com/pubs/pubs/issues/138)
- Allow utf8 citekeys [(#133)](https://github.com/pubs/pubs/issues/133) - Allow utf8 citekeys [(#133)](https://github.com/pubs/pubs/issues/133)
- Adds tag list completion in `pubs add -t ` [(#130)](https://github.com/pubs/pubs/issues/130) - Adds tag list completion in `pubs add -t ` [(#130)](https://github.com/pubs/pubs/issues/130)
- Wider Travis coverage ([#107](https://github.com/pubs/pubs/issues/107) and [#108](https://github.com/pubs/pubs/issues/108)) - Wider Travis coverage ([#107](https://github.com/pubs/pubs/issues/107) and [#108](https://github.com/pubs/pubs/issues/108))
- Uses bibtexparser bwriter instead of internal encoder and adds `--ignore-fields` option to export. [(#106)](https://github.com/pubs/pubs/issues/106) - Uses bibtexparser bwriter instead of internal encoder and adds `--ignore-fields` option to export. [(#106)](https://github.com/pubs/pubs/issues/106)
- Configurable alias descriptions ([#104](https://github.com/pubs/pubs/issues/104) by [wflynny](https://github.com/wflynny)) - Configurable alias descriptions ([#104](https://github.com/pubs/pubs/issues/104) by [wflynny](https://github.com/wflynny))
- Support year ranges in query [(#102)](https://github.com/pubs/pubs/issues/102) - Support year ranges in query [(#102)](https://github.com/pubs/pubs/issues/102)
- Tests can now be run with `python setup.py test` [#155](https://github.com/pubs/pubs/issues/155) - Tests can now be run with `python setup.py test` [#155](https://github.com/pubs/pubs/issues/155)
### Fixed bugs ### Fixed bugs
- [[#144]](https://github.com/pubs/pubs/issues/144) More robust handling of the `doc_add` options [(#159)](https://github.com/pubs/pubs/pull/159) - [[#144]](https://github.com/pubs/pubs/issues/144) More robust handling of the `doc_add` options [(#159)](https://github.com/pubs/pubs/pull/159)
- [[#149]](https://github.com/pubs/pubs/issues/149) More robust handling of parsing and citekey errors [(#87)](https://github.com/pubs/pubs/pull/87) - [[#149]](https://github.com/pubs/pubs/issues/149) More robust handling of parsing and citekey errors [(#87)](https://github.com/pubs/pubs/pull/87)
- [[#148]](https://github.com/pubs/pubs/issues/148) Fix compatibility with Pyfakefs 3.7 [(#151)](https://github.com/pubs/pubs/pull/151) - [[#148]](https://github.com/pubs/pubs/issues/148) Fix compatibility with Pyfakefs 3.7 [(#151)](https://github.com/pubs/pubs/pull/151)
- [[#95]](https://github.com/pubs/pubs/issues/95) Error message when editor is missing [(#141)](https://github.com/pubs/pubs/issues/141) - [[#95]](https://github.com/pubs/pubs/issues/95) Error message when editor is missing [(#141)](https://github.com/pubs/pubs/issues/141)
- Fixes tests for printing help on `--help` and without argument. [(#137)](https://github.com/pubs/pubs/issues/137) - Fixes tests for printing help on `--help` and without argument. [(#137)](https://github.com/pubs/pubs/issues/137)
- [[#126]](https://github.com/pubs/pubs/issues/126) Removes journal customization [(#127)](https://github.com/pubs/pubs/issues/127) - [[#126]](https://github.com/pubs/pubs/issues/126) Removes journal customization [(#127)](https://github.com/pubs/pubs/issues/127)
- Fixes Travis failure on installing python3 for OSX [(#125)](https://github.com/pubs/pubs/issues/125) - Fixes Travis failure on installing python3 for OSX [(#125)](https://github.com/pubs/pubs/issues/125)
- [[#119]](https://github.com/pubs/pubs/issues/119) Removes link and DOI customization. [(#124)](https://github.com/pubs/pubs/issues/124) - [[#119]](https://github.com/pubs/pubs/issues/119) Removes link and DOI customization. [(#124)](https://github.com/pubs/pubs/issues/124)
- [[#122]](https://github.com/pubs/pubs/issues/122) Fixes common strings [(#123)](https://github.com/pubs/pubs/issues/123) - [[#122]](https://github.com/pubs/pubs/issues/122) Fixes common strings [(#123)](https://github.com/pubs/pubs/issues/123)
- [[#28]](https://github.com/pubs/pubs/issues/28) allow utf8 in citekeys [(#120)](https://github.com/pubs/pubs/issues/120) - [[#28]](https://github.com/pubs/pubs/issues/28) allow utf8 in citekeys [(#120)](https://github.com/pubs/pubs/issues/120)
- Fixes field orders to use 'url' and fixes broken test. [(#118)](https://github.com/pubs/pubs/issues/118) - Fixes field orders to use 'url' and fixes broken test. [(#118)](https://github.com/pubs/pubs/issues/118)
- [[#25]](https://github.com/pubs/pubs/issues/25) Fix bibtex testcase [(#117)](https://github.com/pubs/pubs/issues/117) - [[#25]](https://github.com/pubs/pubs/issues/25) Fix bibtex testcase [(#117)](https://github.com/pubs/pubs/issues/117)
- [[#103]](https://github.com/pubs/pubs/issues/103) Fixes unicode comparison [(#116)](https://github.com/pubs/pubs/issues/116) - [[#103]](https://github.com/pubs/pubs/issues/103) Fixes unicode comparison [(#116)](https://github.com/pubs/pubs/issues/116)
- [[#95]](https://github.com/pubs/pubs/issues/95) robust handling of DOIs ([#105](https://github.com/pubs/pubs/issues/105) by [wflynny](https://github.com/wflynny)) - [[#95]](https://github.com/pubs/pubs/issues/95) robust handling of DOIs ([#105](https://github.com/pubs/pubs/issues/105) by [wflynny](https://github.com/wflynny))
- [[#99]](https://github.com/pubs/pubs/issues/99) Print help when no subcommand is provided ([#100](https://github.com/pubs/pubs/issues/100) by [wflynny](https://github.com/wflynny)) - [[#99]](https://github.com/pubs/pubs/issues/99) Print help when no subcommand is provided ([#100](https://github.com/pubs/pubs/issues/100) by [wflynny](https://github.com/wflynny))
- Fix defaults not used in config. [(#97)](https://github.com/pubs/pubs/issues/97) - Fix defaults not used in config. [(#97)](https://github.com/pubs/pubs/issues/97)
- Fixes content not read from urls because of call to `os.abspath` [(#96)](https://github.com/pubs/pubs/issues/96) - Fixes content not read from urls because of call to `os.abspath` [(#96)](https://github.com/pubs/pubs/issues/96)
- [[#93]](https://github.com/pubs/pubs/issues/93) actually save the modifications on `edit -m`. [(#94)](https://github.com/pubs/pubs/issues/94) - [[#93]](https://github.com/pubs/pubs/issues/93) actually save the modifications on `edit -m`. [(#94)](https://github.com/pubs/pubs/issues/94)
- [[#88]](https://github.com/pubs/pubs/issues/88) Adds proper escaping for - [[#88]](https://github.com/pubs/pubs/issues/88) Adds proper escaping for
arguments in alias plugin. [(#91)](https://github.com/pubs/pubs/issues/91) arguments in alias plugin. [(#91)](https://github.com/pubs/pubs/issues/91)

@ -20,6 +20,6 @@ six
# those are the additional packages required to run the tests # those are the additional packages required to run the tests
pyfakefs pyfakefs
certifi certifi
ddt ddt>=1.4.1
mock mock
pytest # optional (python setup.py test works without it), but possible nonetheless pytest

@ -16,7 +16,7 @@ from .p3 import ustr, uchr
# Citekey stuff # Citekey stuff
TYPE_KEY = 'type' TYPE_KEY = 'ENTRYTYPE'
CONTROL_CHARS = ''.join(map(uchr, list(range(0, 32)) + list(range(127, 160)))) CONTROL_CHARS = ''.join(map(uchr, list(range(0, 32)) + list(range(127, 160))))
CITEKEY_FORBIDDEN_CHARS = '@\'\\,#}{~%/ ' # '/' is OK for bibtex but forbidden CITEKEY_FORBIDDEN_CHARS = '@\'\\,#}{~%/ ' # '/' is OK for bibtex but forbidden

@ -144,7 +144,7 @@ def setup(conf, force_colors=False):
# undye # 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"""

@ -6,6 +6,7 @@ from .. import p3
from .. import bibstruct from .. import bibstruct
from .. import content from .. import content
from .. import repo from .. import repo
from .. import color
from .. import paper from .. import paper
from .. import templates from .. import templates
from .. import apis from .. import apis
@ -150,15 +151,17 @@ def command(conf, args):
doc_add = conf['main']['doc_add'] doc_add = conf['main']['doc_add']
rp.push_paper(p) rp.push_paper(p)
ui.message('added to pubs:\n{}'.format(pretty.paper_oneliner(p))) ui.message('added to pubs:\n{}'.format(pretty.paper_oneliner(p, max_authors=conf['main']['max_authors'])))
if docfile is not None: if docfile is not None:
rp.push_doc(p.citekey, docfile, copy=(doc_add in ('copy', 'move'))) rp.push_doc_paper(p, docfile, copy=(doc_add in ('copy', 'move')))
if doc_add in ('move', 'copy'):
if doc_add == 'move' and content.content_type(docfile) != 'url': if doc_add == 'move' and content.content_type(docfile) != 'url':
content.remove_file(docfile) content.remove_file(docfile)
if doc_add == 'move': docpath = content.system_path(rp.databroker.real_docpath(p.docpath))
ui.message('{} was moved to the pubs repository.'.format(docfile)) verb = 'moved' if doc_add == 'move' else 'copied'
elif doc_add == 'copy': ui.message('{} was {} to {} inside the pubs repository.'.format(color.dye_out(docfile, 'filepath'), verb,
ui.message('{} was copied to the pubs repository.'.format(docfile)) color.dye_out(docpath, 'filepath')))
rp.close() rp.close()

@ -34,7 +34,7 @@ def parser(subparsers, conf):
).completer = CiteKeyCompletion(conf) ).completer = CiteKeyCompletion(conf)
add_exclusives = add_parser.add_mutually_exclusive_group() add_exclusives = add_parser.add_mutually_exclusive_group()
add_exclusives.add_argument( add_exclusives.add_argument(
'-L', '--link', action='store_false', dest='link', default=False, '-L', '--link', action='store_true', dest='link', default=False,
help='do not copy document files, just create a link') help='do not copy document files, just create a link')
add_exclusives.add_argument( add_exclusives.add_argument(
'-M', '--move', action='store_true', dest='move', default=False, '-M', '--move', action='store_true', dest='move', default=False,
@ -71,7 +71,7 @@ def command(conf, args):
# ui.exit() # ui.exit()
if args.action == 'add': if args.action == 'add':
citekey = resolve_citekey(rp, args.citekey[0], ui=ui, exit_on_fail=True) citekey = resolve_citekey(rp, conf, args.citekey[0], ui=ui, exit_on_fail=True)
paper = rp.pull_paper(citekey) paper = rp.pull_paper(citekey)
if paper.docpath is not None and not args.force: if paper.docpath is not None and not args.force:
@ -92,13 +92,14 @@ def command(conf, args):
if not args.link and args.move: if not args.link and args.move:
content.remove_file(document) content.remove_file(document)
# FIXME: coherence with add command, the destination location should be given when copying/moving.
ui.message('{} added to {}'.format( ui.message('{} added to {}'.format(
color.dye_out(document, 'filepath'), color.dye_out(document, 'filepath'),
color.dye_out(paper.citekey, 'citekey'))) color.dye_out(paper.citekey, 'citekey')))
elif args.action == 'remove': elif args.action == 'remove':
for key in resolve_citekey_list(rp, args.citekeys, ui=ui, exit_on_fail=True): for key in resolve_citekey_list(rp, conf, args.citekeys, ui=ui, exit_on_fail=True):
paper = rp.pull_paper(key) paper = rp.pull_paper(key)
# if there is no document (and the user cares) -> inform + continue # if there is no document (and the user cares) -> inform + continue
@ -126,7 +127,7 @@ def command(conf, args):
color.dye_err(args.path[0], 'filepath'))) color.dye_err(args.path[0], 'filepath')))
ui.exit(1) ui.exit(1)
for key in resolve_citekey_list(rp, args.citekeys, ui=ui, exit_on_fail=True): for key in resolve_citekey_list(rp, conf, args.citekeys, ui=ui, exit_on_fail=True):
try: try:
paper = rp.pull_paper(key) paper = rp.pull_paper(key)
doc = paper.docpath doc = paper.docpath
@ -143,7 +144,7 @@ def command(conf, args):
elif args.action == 'open': elif args.action == 'open':
with_command = args.cmd with_command = args.cmd
citekey = resolve_citekey(rp, args.citekey[0], ui=ui, exit_on_fail=True) citekey = resolve_citekey(rp, conf, args.citekey[0], ui=ui, exit_on_fail=True)
paper = rp.pull_paper(citekey) paper = rp.pull_paper(citekey)
if paper.docpath is None: if paper.docpath is None:

@ -2,6 +2,7 @@ from __future__ import unicode_literals
from ..paper import Paper from ..paper import Paper
from .. import repo from .. import repo
from .. import color
from ..uis import get_ui from ..uis import get_ui
from ..endecoder import EnDecoder from ..endecoder import EnDecoder
@ -29,7 +30,7 @@ def command(conf, args):
meta = args.meta meta = args.meta
rp = repo.Repository(conf) rp = repo.Repository(conf)
citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True) citekey = resolve_citekey(rp, conf, args.citekey, ui=ui, exit_on_fail=True)
paper = rp.pull_paper(citekey) paper = rp.pull_paper(citekey)
coder = EnDecoder() coder = EnDecoder()
@ -55,17 +56,18 @@ def command(conf, args):
new_paper = Paper(paper.citekey, paper.bibdata, new_paper = Paper(paper.citekey, paper.bibdata,
metadata=content) metadata=content)
rp.push_paper(new_paper, overwrite=True, event=False) rp.push_paper(new_paper, overwrite=True, event=False)
ui.info(('The metadata of paper `{}` was successfully ' ui.info(("The metadata of paper '{}' was successfully "
'edited.'.format(citekey))) "edited.".format(color.dye_out(citekey, 'citekey'))))
else: else:
new_paper = Paper.from_bibentry(content, new_paper = Paper.from_bibentry(content,
metadata=paper.metadata) metadata=paper.metadata)
if rp.rename_paper(new_paper, old_citekey=paper.citekey): if rp.rename_paper(new_paper, old_citekey=paper.citekey):
ui.info(('Paper `{}` was successfully edited and renamed ' ui.info(("Paper '{}' was successfully edited and renamed "
'as `{}`.'.format(citekey, new_paper.citekey))) "as '{}'.".format(color.dye_out(citekey, 'citekey'),
color.dye_out(new_paper.citekey, 'citekey'))))
else: else:
ui.info(('Paper `{}` was successfully edited.'.format( ui.info(("Paper '{}' was successfully edited.".format(
citekey))) color.dye_out(citekey, 'citekey'))))
break break
except coder.BibDecodingError: except coder.BibDecodingError:
@ -84,7 +86,7 @@ def command(conf, args):
break break
elif choice == 'overwrite': elif choice == 'overwrite':
paper = rp.push_paper(paper, overwrite=True) paper = rp.push_paper(paper, overwrite=True)
ui.info(('Paper `{}` was overwritten.'.format(citekey))) ui.info(('Paper `{}` was overwritten.'.format(color.dye_out(citekey, 'citekey'))))
break break
# else edit again # else edit again
# Also handle malformed bibtex and metadata # Also handle malformed bibtex and metadata

@ -46,7 +46,7 @@ def command(conf, args):
if len(args.citekeys) < 1: if len(args.citekeys) < 1:
papers = rp.all_papers() papers = rp.all_papers()
else: else:
for key in resolve_citekey_list(repo=rp, citekeys=args.citekeys, ui=ui, exit_on_fail=True): for key in resolve_citekey_list(rp, conf, args.citekeys, ui=ui, exit_on_fail=True):
papers.append(rp.pull_paper(key)) papers.append(rp.pull_paper(key))
bib = {} bib = {}

@ -54,7 +54,7 @@ def command(conf, args):
papers = sorted(papers, key=date_added) papers = sorted(papers, key=date_added)
if len(papers) > 0: if len(papers) > 0:
ui.message('\n'.join( ui.message('\n'.join(
pretty.paper_oneliner(p, citekey_only=args.citekeys) pretty.paper_oneliner(p, citekey_only=args.citekeys, max_authors=conf['main']['max_authors'])
for p in papers)) for p in papers))
rp.close() rp.close()

@ -23,7 +23,7 @@ def command(conf, args):
ui = get_ui() ui = get_ui()
rp = repo.Repository(conf) rp = repo.Repository(conf)
citekey = resolve_citekey(rp, args.citekey, ui=ui, exit_on_fail=True) citekey = resolve_citekey(rp, conf, args.citekey, ui=ui, exit_on_fail=True)
notepath = rp.databroker.real_notepath(citekey, rp.conf['main']['note_extension']) notepath = rp.databroker.real_notepath(citekey, rp.conf['main']['note_extension'])
if args.append is None: if args.append is None:
ui.edit_file(notepath, temporary=False) ui.edit_file(notepath, temporary=False)

@ -2,6 +2,7 @@ from __future__ import unicode_literals
from .. import repo from .. import repo
from .. import color from .. import color
from .. import pretty
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, u_maybe from ..p3 import ustr, u_maybe
@ -24,12 +25,16 @@ def command(conf, args):
force = args.force force = args.force
rp = repo.Repository(conf) rp = repo.Repository(conf)
keys = resolve_citekey_list(repo=rp, citekeys=args.citekeys, ui=ui, exit_on_fail=True) keys = resolve_citekey_list(rp, conf, args.citekeys, ui=ui, exit_on_fail=True)
plural = 's' if len(keys) > 1 else ''
if force is None: if force is None:
are_you_sure = (("Are you sure you want to delete the publication(s) [{}]" to_remove_str = '\n'.join(pretty.paper_oneliner(rp.pull_paper(key),
" (this will also delete associated documents)?") max_authors=conf['main']['max_authors'])
.format(', '.join([color.dye_out(c, 'citekey') for c in args.citekeys]))) for key in keys)
are_you_sure = (("Are you sure you want to delete the following publication{}"
" (this will also delete associated documents)?:\n{}\n")
.format(plural, to_remove_str))
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:
failed = False # Whether something failed failed = False # Whether something failed
@ -42,11 +47,12 @@ def command(conf, args):
if failed: if failed:
ui.exit() # Exit with nonzero error code ui.exit() # Exit with nonzero error code
else: else:
ui.message('The publication(s) [{}] were removed'.format(
ui.message('The publication{} {} were removed'.format(plural,
', '.join([color.dye_out(c, 'citekey') for c in keys]))) ', '.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{} {} were {} removed'.format(plural,
', '.join([color.dye_out(c, 'citekey') for c in keys]), ', '.join([color.dye_out(c, 'citekey') for c in keys]),
color.dye_out('not','bold'))) color.dye_out('not','bold')))

@ -26,7 +26,7 @@ def command(conf, args):
rp = repo.Repository(conf) rp = repo.Repository(conf)
# TODO: here should be a test whether the new citekey is valid # TODO: here should be a test whether the new citekey is valid
key = resolve_citekey(repo=rp, citekey=args.citekey, ui=ui, exit_on_fail=True) key = resolve_citekey(rp, conf, args.citekey, ui=ui, exit_on_fail=True)
paper = rp.pull_paper(key) paper = rp.pull_paper(key)
rp.rename_paper(paper, args.new_citekey) rp.rename_paper(paper, args.new_citekey)
ui.message("The '{}' citekey has been renamed into '{}'".format( ui.message("The '{}' citekey has been renamed into '{}'".format(

@ -22,7 +22,7 @@ def command(conf, args):
else: else:
doc_count = sum([0 if p.docpath is None else 1 for p in papers]) doc_count = sum([0 if p.docpath is None else 1 for p in papers])
tag_count = len(list(rp.get_tags())) tag_count = len(list(rp.get_tags()))
papers_with_tags = sum([0 if p.tags else 1 for p in papers]) papers_with_tags = sum([1 if p.tags else 0 for p in papers])
ui.message(color.dye_out('Repository statistics:', 'bold')) ui.message(color.dye_out('Repository statistics:', 'bold'))
ui.message('Total papers: {}, {} ({}) have a document attached'.format( ui.message('Total papers: {}, {} ({}) have a document attached'.format(

@ -55,6 +55,8 @@ def _parse_tag_seq(s):
if last != 0: if last != 0:
raise ValueError('could not match tag expression') raise ValueError('could not match tag expression')
else: else:
tag = s[last:(m.start())]
if len(tag) > 0:
tags.append(s[last:(m.start())]) tags.append(s[last:(m.start())])
last = m.start() last = m.start()
if last == len(s): if last == len(s):
@ -89,7 +91,7 @@ def command(conf, args):
else: else:
not_citekey = False not_citekey = False
try: try:
citekeyOrTag = resolve_citekey(repo=rp, citekey=citekeyOrTag, ui=ui, exit_on_fail=True) citekeyOrTag = resolve_citekey(rp, conf, citekeyOrTag, ui=ui, exit_on_fail=True)
except SystemExit: except SystemExit:
not_citekey = True not_citekey = True
if not not_citekey: if not not_citekey:
@ -117,7 +119,7 @@ def command(conf, args):
len(p.tags.intersection(excluded)) == 0): len(p.tags.intersection(excluded)) == 0):
papers_list.append(p) papers_list.append(p)
ui.message('\n'.join(pretty.paper_oneliner(p) ui.message('\n'.join(pretty.paper_oneliner(p, max_authors=conf['main']['max_authors'])
for p in papers_list)) for p in papers_list))
rp.close() rp.close()

@ -22,7 +22,7 @@ def command(conf, args):
ui = get_ui() ui = get_ui()
rp = repo.Repository(conf) rp = repo.Repository(conf)
for key in resolve_citekey_list(rp, args.citekey, ui=ui, exit_on_fail=False): for key in resolve_citekey_list(rp, conf, args.citekey, ui=ui, exit_on_fail=False):
try: try:
paper = rp.pull_paper(key) paper = rp.pull_paper(key)
url = paper.bibdata['url'] url = paper.bibdata['url']

@ -27,6 +27,10 @@ edit_cmd = string(default='')
# Which default extension to use when creating a note file. # Which default extension to use when creating a note file.
note_extension = string(default='txt') note_extension = string(default='txt')
# How many authors to display when displaying a citation. If there are more
# authors, only the first author is diplayed followed by 'et al.'.
max_authors = integer(default=3)
# If true debug mode is on which means exceptions are not catched and # If true debug mode is on which means exceptions are not catched and
# the full python stack is printed. # the full python stack is printed.
debug = boolean(default=False) debug = boolean(default=False)
@ -115,6 +119,9 @@ active = force_list(default=list('alias'))
# command = !pubs list -k | wc -l # command = !pubs list -k | wc -l
# description = lists number of pubs in repo # description = lists number of pubs in repo
# To use commas in the description, wrap them in a "" string. For example:
# description = "lists number of pubs in repo, greets the user afterward"
[[git]] [[git]]
# The git plugin will commit changes to the repository in a git repository # The git plugin will commit changes to the repository in a git repository
# created at the root of the pubs directory. All detected changes will be # created at the root of the pubs directory. All detected changes will be

@ -114,7 +114,7 @@ def write_file(filepath, data, mode='w'):
def content_type(path): def content_type(path):
parsed = urlparse(path) parsed = urlparse(path)
if parsed.scheme == 'http': if parsed.scheme in ('http', 'https'):
return 'url' return 'url'
else: else:
return 'file' return 'file'

@ -51,7 +51,6 @@ def customizations(record):
""" """
# record = bp.customization.convert_to_unicode(record) # transform \& into & ones, messing-up latex # record = bp.customization.convert_to_unicode(record) # transform \& into & ones, messing-up latex
record = bp.customization.type(record)
record = bp.customization.author(record) record = bp.customization.author(record)
record = bp.customization.editor(record) record = bp.customization.editor(record)
record = bp.customization.keyword(record) record = bp.customization.keyword(record)
@ -119,18 +118,18 @@ class EnDecoder(object):
keyword for keyword in entry['keyword']) keyword for keyword in entry['keyword'])
return entry return entry
def decode_bibdata(self, bibdata): def decode_bibdata(self, bibstr):
"""Decodes bibdata from string. """Decodes bibdata from string.
If the decoding fails, returns a BibDecodingError. If the decoding fails, returns a BibDecodingError.
""" """
if len(bibdata) == 0: if len(bibstr) == 0:
error_msg = 'parsing error: the provided string has length zero.' error_msg = 'parsing error: the provided string has length zero.'
raise self.BibDecodingError(error_msg, bibdata) raise self.BibDecodingError(error_msg, bibstr)
try: try:
entries = bp.bparser.BibTexParser( entries = bp.bparser.BibTexParser(
bibdata, common_strings=True, customization=customizations, bibstr, common_strings=True, customization=customizations,
homogenize_fields=True).get_entry_dict() homogenize_fields=True, ignore_nonstandard_types=False).get_entry_dict()
# Remove id from bibtexparser attribute which is stored as citekey # Remove id from bibtexparser attribute which is stored as citekey
for e in entries: for e in entries:
entries[e].pop(BP_ID_KEY) entries[e].pop(BP_ID_KEY)
@ -147,13 +146,13 @@ class EnDecoder(object):
return entries return entries
else: else:
raise self.BibDecodingError(('no valid entry found in the provided data: ' raise self.BibDecodingError(('no valid entry found in the provided data: '
' {}').format(bibdata), bibdata) ' {}').format(bibstr), bibstr)
except (pyparsing.ParseException, pyparsing.ParseSyntaxException) as e: except (pyparsing.ParseException, pyparsing.ParseSyntaxException) as e:
error_msg = self._format_parsing_error(e) error_msg = self._format_parsing_error(e)
raise self.BibDecodingError(error_msg, bibdata) raise self.BibDecodingError(error_msg, bibstr)
except bibtexparser.bibdatabase.UndefinedString as e: except bibtexparser.bibdatabase.UndefinedString as e:
error_msg = 'parsing error: undefined string in provided data: {}'.format(e) error_msg = 'parsing error: undefined string in provided data: {}'.format(e)
raise self.BibDecodingError(error_msg, bibdata) raise self.BibDecodingError(error_msg, bibstr)
@classmethod @classmethod
def _format_parsing_error(cls, e): def _format_parsing_error(cls, e):

@ -16,7 +16,7 @@ def filter_filename(filename, ext):
""" Return the filename without the extension if the extension matches ext. """ Return the filename without the extension if the extension matches ext.
Otherwise return None Otherwise return None
""" """
pattern = '.*\{}$'.format(ext) pattern = '.*\\{}$'.format(ext)
if re.match(pattern, filename) is not None: if re.match(pattern, filename) is not None:
return u_maybe(filename[:-len(ext)]) return u_maybe(filename[:-len(ext)])
@ -154,7 +154,7 @@ class DocBroker(object):
def __init__(self, directory, scheme='docsdir', subdir='doc'): def __init__(self, directory, scheme='docsdir', subdir='doc'):
self.scheme = scheme self.scheme = scheme
self.docdir = os.path.join(directory, subdir) self.docdir = os.path.expanduser(os.path.join(directory, subdir))
if not check_directory(self.docdir, fail=False): if not check_directory(self.docdir, fail=False):
os.mkdir(system_path(self.docdir)) os.mkdir(system_path(self.docdir))

@ -16,11 +16,13 @@ GITIGNORE = """# files or directories for the git plugin to ignore
class GitPlugin(PapersPlugin): class GitPlugin(PapersPlugin):
"""The git plugin creates a git repository in the pubs directory and commit the changes """Make the pubs repository also a git repository.
to the pubs repository everytime a paper is modified.
It also add the `pubs git` subcommand, so git commands can be executed in the git repository The git plugin creates a git repository in the pubs directory
from the command line. and commit the changes to the pubs repository.
It also add the `pubs git` subcommand, so git commands can be executed
in the git repository from the command line.
""" """
name = 'git' name = 'git'
@ -72,17 +74,18 @@ class GitPlugin(PapersPlugin):
""" """
colorize = ' -c color.ui=always' if self.force_color else '' colorize = ' -c color.ui=always' if self.force_color else ''
git_cmd = 'git -C {}{} {}'.format(self.pubsdir, colorize, cmd) git_cmd = 'git -C {}{} {}'.format(self.pubsdir, colorize, cmd)
#print(git_cmd)
p = Popen(git_cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=True) p = Popen(git_cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=True)
output, err = p.communicate(input_stdin) output, err = p.communicate(input_stdin)
p.wait() p.wait()
if p.returncode != 0: if p.returncode != 0:
raise RuntimeError('The git plugin encountered an error when running the git command:\n' + raise RuntimeError((
'{}\n\nReturned output:\n{}\n'.format(git_cmd, output.decode('utf-8')) + 'The git plugin encountered an error when running the git command:\n'
'If needed, you may fix the state of the {} git repository '.format(self.pubsdir) + '{}\n\n'
'manually.\nIf relevant, you may submit a bug report at ' + 'Returned output:\n{}\n'
'https://github.com/pubs/pubs/issues') 'If needed, you may fix the state of the {} git repository manually.\n'
'If relevant, you may submit a bug report at https://github.com/pubs/pubs/issues'
).format(git_cmd, output.decode('utf-8'), self.pubsdir))
elif command: elif command:
self.ui.message(output.decode('utf-8'), end='') self.ui.message(output.decode('utf-8'), end='')
elif not self.quiet: elif not self.quiet:
@ -101,6 +104,7 @@ def paper_change_event(event):
event_desc = event_desc.replace(a, b) event_desc = event_desc.replace(a, b)
git.list_of_changes.append(event_desc) git.list_of_changes.append(event_desc)
@PostCommandEvent.listen() @PostCommandEvent.listen()
def git_commit(event): def git_commit(event):
if GitPlugin.is_loaded(): if GitPlugin.is_loaded():

@ -23,19 +23,24 @@ def person_repr(p):
' '.join(p.lineage(abbr=True))] if s) ' '.join(p.lineage(abbr=True))] if s)
def short_authors(bibdata): def short_authors(bibdata, max_authors=3):
"""
:param max_authors: number of authors to display completely. Additional authors will be
represented by 'et al.'.
"""
try: try:
authors = [p for p in bibdata['author']] authors = [p for p in bibdata['author']]
if len(authors) < 3: if 0 < max_authors < len(authors):
return ' and '.join(authors) authors_str = '{} et al.'.format(authors[0])
else: else:
return authors[0] + (' et al.' if len(authors) > 1 else '') authors_str = ' and '.join(authors)
return authors_str
except KeyError: # When no author is defined except KeyError: # When no author is defined
return '' return ''
def bib_oneliner(bibdata): def bib_oneliner(bibdata, max_authors=3):
authors = short_authors(bibdata) authors = short_authors(bibdata, max_authors=max_authors)
journal = '' journal = ''
if 'journal' in bibdata: if 'journal' in bibdata:
journal = ' ' + bibdata['journal'] journal = ' ' + bibdata['journal']
@ -60,11 +65,11 @@ def bib_desc(bib_data):
return s return s
def paper_oneliner(p, citekey_only=False): def paper_oneliner(p, citekey_only=False, max_authors=3):
if citekey_only: if citekey_only:
return p.citekey return p.citekey
else: else:
bibdesc = bib_oneliner(p.get_unicode_bibdata()) bibdesc = bib_oneliner(p.get_unicode_bibdata(), max_authors=max_authors)
doc_str = '' doc_str = ''
if p.docpath is not None: if p.docpath is not None:
doc_extension = os.path.splitext(p.docpath)[1] doc_extension = os.path.splitext(p.docpath)[1]

@ -182,17 +182,22 @@ class Repository(object):
events.RenameEvent(paper, old_citekey).send() events.RenameEvent(paper, old_citekey).send()
return True return True
def push_doc(self, citekey, docfile, copy=None): def push_doc(self, citekey, docfile, copy=None):
p = self.pull_paper(citekey) p = self.pull_paper(citekey)
return self.push_doc_paper(p, docfile, copy=copy)
def push_doc_paper(self, paper, docfile, copy=None):
"""Same as push_doc, only the Paper instance is provided rather than the citekey"""
if copy is None: if copy is None:
copy = self.conf['main']['doc_add'] in ('copy', 'move') copy = self.conf['main']['doc_add'] in ('copy', 'move')
if copy: if copy:
docfile = self.databroker.add_doc(citekey, docfile) docfile = self.databroker.add_doc(paper.citekey, docfile)
else: else:
docfile = system_path(docfile) docfile = system_path(docfile)
p.docpath = docfile paper.docpath = docfile
self.push_paper(p, overwrite=True, event=False) self.push_paper(paper, overwrite=True, event=False)
events.DocAddEvent(citekey).send() events.DocAddEvent(paper.citekey).send()
def unique_citekey(self, base_key, bibentry): def unique_citekey(self, base_key, bibentry):
"""Create a unique citekey for a given base key. """Create a unique citekey for a given base key.

@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals
import os import os
import sys import sys
import shlex import shlex
import errno
import locale import locale
import codecs import codecs
import tempfile import tempfile
@ -237,7 +238,7 @@ class InputUI(PrintUI):
try: try:
subprocess.call(cmd) subprocess.call(cmd)
except OSError as e: except OSError as e:
if e.errno == os.errno.ENOENT: if e.errno == errno.ENOENT:
self.error(("Error while calling editor '{}'. The editor may " self.error(("Error while calling editor '{}'. The editor may "
"not be present. You can change the text editor " "not be present. You can change the text editor "
"that pubs uses by setting the $EDITOR environment " "that pubs uses by setting the $EDITOR environment "

@ -7,7 +7,7 @@ from . import color
from . import pretty from . import pretty
def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True): def resolve_citekey(repo, conf, citekey, ui=None, exit_on_fail=True):
"""Check that a citekey exists, or autocompletes it if not ambiguous. """Check that a citekey exists, or autocompletes it if not ambiguous.
:returns found citekey :returns found citekey
""" """
@ -29,22 +29,23 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True):
elif citekey not in citekeys: elif citekey not in citekeys:
if ui is not None: if ui is not None:
citekeys = sorted(citekeys) citekeys = sorted(citekeys)
ui.error("Be more specific; '{}' matches multiples " msg = ["Be more specific; '{}' matches multiples 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(' {}'.format(pretty.paper_oneliner(p))) paper_str = pretty.paper_oneliner(p, max_authors=conf['main']['max_authors'])
msg.append(' {}'.format(paper_str))
ui.error('\n'.join(msg))
if exit_on_fail: if exit_on_fail:
ui.exit() ui.exit()
return citekey return citekey
def resolve_citekey_list(repo, citekeys, ui=None, exit_on_fail=True): def resolve_citekey_list(repo, conf, citekeys, ui=None, exit_on_fail=True):
shutdown = False shutdown = False
keys = [] keys = []
for key in citekeys: for key in citekeys:
try: try:
keys.append(resolve_citekey(repo, key, ui, exit_on_fail)) keys.append(resolve_citekey(repo, conf, key, ui=ui, exit_on_fail=exit_on_fail))
except SystemExit: except SystemExit:
shutdown = exit_on_fail shutdown = exit_on_fail
@ -73,11 +74,11 @@ def standardize_doi(doi):
""" """
doi_regexes = ( doi_regexes = (
'(10\.\d{4,9}/[-._;()/:A-z0-9\>\<]+)', r'(10\.\d{4,9}/[-._;()/:A-z0-9\>\<]+)',
'(10.1002/[^\s]+)', r'(10.1002/[^\s]+)',
'(10\.\d{4}/\d+-\d+X?(\d+)\d+<[\d\w]+:[\d\w]*>\d+.\d+.\w+;\d)', r'(10\.\d{4}/\d+-\d+X?(\d+)\d+<[\d\w]+:[\d\w]*>\d+.\d+.\w+;\d)',
'(10\.1021/\w\w\d+\+)', r'(10\.1021/\w\w\d+\+)',
'(10\.1207/[\w\d]+\&\d+_\d+)') r'(10\.1207/[\w\d]+\&\d+_\d+)')
doi_pattern = re.compile('|'.join(doi_regexes)) doi_pattern = re.compile('|'.join(doi_regexes))
match = doi_pattern.search(doi) match = doi_pattern.search(doi)

@ -15,24 +15,24 @@ correct bugs, but have no short-term plans to add major features to it. Pubs doe
is supposed to do: help us do science, so now we are mostly doing that. is supposed to do: help us do science, so now we are mostly doing that.
**Notice:** pubs is relatively stable but comes with no warranty; do keep backups of your data. **Notice:** pubs is relatively stable but comes with no warranty; do keep backups of your data.
**Notice:** pubs currently works with Python 2.7, but support will be dropped as soon as maintaining it becomes tedious. **Notice:** pubs currently works with Python 2.7, but support is being dropped (tests are not run anymore).
## Installation ## Installation
You can install the latest stable version of `pubs` through Pypi, with: You can install the latest stable version of `pubs` through Pypi, with:
``` ```shell
pip install pubs pip install pubs
``` ```
Alternatively, you can: Alternatively, you can:
- install the latest development version with pip: - install the latest development version with pip:
``` ```shell
pip install --upgrade git+https://github.com/pubs/pubs pip install --upgrade git+https://github.com/pubs/pubs
``` ```
- clone the repository and install it manually: - clone the repository and install it manually:
``` ```shell
git clone https://github.com/pubs/pubs git clone https://github.com/pubs/pubs
cd pubs cd pubs
python setup.py install [--user] python setup.py install [--user]
@ -44,32 +44,32 @@ Arch Linux users can also use the [pubs-git](https://aur.archlinux.org/packages/
## Getting started ## Getting started
Create your library (by default, goes to `~/.pubs/`). Create your library (by default, goes to `~/.pubs/`).
``` ```shell
pubs init pubs init
``` ```
Import existing data from bibtex (pubs will try to automatically copy documents defined as 'file' in bibtex): Import existing data from bibtex (pubs will try to automatically copy documents defined as 'file' in bibtex):
``` ```shell
pubs import path/to/collection.bib pubs import path/to/collection.bib
``` ```
or for a .bib file containing a single reference: or for a .bib file containing a single reference:
``` ```shell
pubs add reference.bib -d article.pdf pubs add reference.bib -d article.pdf
``` ```
pubs can also automatically retrieve the bibtex from a doi: pubs can also automatically retrieve the bibtex from a doi:
``` ```shell
pubs add -D 10.1007/s00422-012-0514-6 -d article.pdf pubs add -D 10.1007/s00422-012-0514-6 -d article.pdf
``` ```
or an ISBN (dashes are ignored): or an ISBN (dashes are ignored):
``` ```shell
pubs add -I 978-0822324669 -d article.pdf pubs add -I 978-0822324669 -d article.pdf
``` ```
or an arXiv id (automatically downloading arXiv article is in the works): or an arXiv id (automatically downloading arXiv article is in the works):
``` ```shell
pubs add -X math/9501234 -d article.pdf pubs add -X math/9501234 -d article.pdf
``` ```
@ -79,7 +79,7 @@ or an arXiv id (automatically downloading arXiv article is in the works):
If you use latex, you can automatize references, by running `pubs export > references.bib` each time you update your library, which also fits well as a `makefile` rule. If you use latex, you can automatize references, by running `pubs export > references.bib` each time you update your library, which also fits well as a `makefile` rule.
This ensures that your reference file is always up-to-date; you can cite a paper in your manuscript a soon as you add it in pubs. This means that if you have, for instance, a DOI on a webpage, you only need to do: This ensures that your reference file is always up-to-date; you can cite a paper in your manuscript a soon as you add it in pubs. This means that if you have, for instance, a DOI on a webpage, you only need to do:
``` ```shell
pubs add -D 10.1007/s00422-012-0514-6 pubs add -D 10.1007/s00422-012-0514-6
``` ```
@ -89,12 +89,12 @@ and then add `\cite{Loeb_2012}` in your manuscript. After exporting the bibliogr
## Document management ## Document management
You can attach a document to a reference: You can attach a document to a reference:
``` ```shell
pubs doc add Loeb2012_downloaded.pdf Loeb_2012 pubs doc add Loeb2012_downloaded.pdf Loeb_2012
``` ```
And open your documents automatically from the command line: And open your documents automatically from the command line:
``` ```shell
pubs doc open Loeb_2012 pubs doc open Loeb_2012
pubs doc open --with lp Loeb_2012 # Opens the document with `lp` to actually print it. pubs doc open --with lp Loeb_2012 # Opens the document with `lp` to actually print it.
``` ```
@ -111,21 +111,21 @@ Pubs comes with a git plugin that automatically commits your changes. You only n
You can then also conveniently interact with the git repository by using `pubs git <regular git commands>`. You can then also conveniently interact with the git repository by using `pubs git <regular git commands>`.
## Multiple pubs Repository ## Multiple pubs Repositories
You may want to have different pubs repositories, for different projects. To create an alternate repository: You may want to have different pubs repositories, for different projects. To create an alternate repository:
``` ```shell
pubs --config /path/to/config init --pubsdir /path/to/desired_repository_directory pubs --config /path/to/config init --pubsdir /path/to/desired_repository_directory
``` ```
The configuration file and repository will be automatically created. The configuration file and repository will be automatically created.
Then you can add papers to the new repository: Then you can add papers to the new repository:
``` ```shell
pubs --config /path/to/config add -D 10.1007/s00422-012-0514-6 pubs --config /path/to/config add -D 10.1007/s00422-012-0514-6
``` ```
A useful thing might be to define an alias in your shell: A useful thing might be to define an alias in your shell:
``` ```shell
alias pubs2="pubs --config /path/to/config" alias pubs2="pubs --config /path/to/config"
``` ```
and then use `pubs2` as you would use `pubs` directly. Note that you cannot use the alias plugin below to do this. and then use `pubs2` as you would use `pubs` directly. Note that you cannot use the alias plugin below to do this.
@ -141,13 +141,13 @@ You can add custom commands to pubs by defining aliases in your configuration fi
count = !pubs list -k "$@" | wc -l count = !pubs list -k "$@" | wc -l
``` ```
The first command defines a new subcommand: `pubs open --with evince` will be executed when `pubs evince` is typed. The first configuration line defines a new subcommand: `pubs open --with evince` will be executed when `pubs evince` is typed.
The second starts with a bang: `!`, and is treated as a shell command. If other arguments are provided they are passed to the shell command as in a script. In the example above the `count` alias can take arguments that are be passed to the `pubs list -k` command, hence enabling filters like `pubs count year:2012`. The second starts with a bang: `!`, which means that it is treated as a shell command. If other arguments are provided they are passed to the shell command as in a script. In the example above the `count` alias can take arguments that are passed over to the `pubs list -k` command, hence enabling filters like `pubs count year:2012`.
## Autocompletion ## Autocompletion
For autocompletion to work, you need the [argcomplete](https://argcomplete.readthedocs.io) Python package, and Bash 4.2 or newer. For activating *bash* or *tsch* completion, consult the [argcomplete documentation](https://argcomplete.readthedocs.io/en/latest/#global-completion). For autocompletion to work, you need the [argcomplete](https://kislyuk.github.io/argcomplete) Python package, and Bash 4.2 or newer. For activating *bash* or *tsch* completion, consult the [argcomplete documentation](https://kislyuk.github.io/argcomplete/#global-completion).
For *zsh* completion, the global activation is not supported but bash completion compatibility can be used for pubs. For that, add the following to your `.zshrc`: For *zsh* completion, the global activation is not supported but bash completion compatibility can be used for pubs. For that, add the following to your `.zshrc`:
```shell ```shell
@ -183,3 +183,7 @@ You can access the self-documented configuration by using `pubs conf`, and all t
- [Shane Stone](https://github.com/shanewstone) - [Shane Stone](https://github.com/shanewstone)
- [Amlesh Sivanantham](http://github.com/zamlz) - [Amlesh Sivanantham](http://github.com/zamlz)
- [DV Klopfenstein](http://github.com/dvklopfenstein) - [DV Klopfenstein](http://github.com/dvklopfenstein)
- [beuerle](https://github.com/beuerle)
- [Jonáš Kulhánek](https://github.com/jkulhanek)
- [Dominik Stańczak](https://github.com/StanczakDominik)
- [Gustavo José de Sousa](https://github.com/guludo)

@ -45,23 +45,22 @@ setup(
}, },
include_package_data=True, include_package_data=True,
install_requires=['pyyaml', 'bibtexparser>=1.0', 'python-dateutil', 'six', install_requires=['pyyaml', 'bibtexparser>=1.0', 'python-dateutil', 'six',
'requests', 'configobj', 'beautifulsoup4', 'feedparser'], 'requests', 'configobj', 'beautifulsoup4', 'feedparser'],
extras_require={'autocompletion': ['argcomplete'], extras_require={'autocompletion': ['argcomplete'],
}, },
python_requires='>=3.6',
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: Science/Research', 'Intended Audience :: Science/Research',
], ],
test_suite='tests', test_suite='tests',
tests_require=['pyfakefs>=3.4', 'mock', 'ddt', 'certifi'], tests_require=['pyfakefs>=3.4', 'mock', 'ddt>=1.4.1', 'certifi', 'pytest'],
# in order to avoid 'zipimport.ZipImportError: bad local file header' # in order to avoid 'zipimport.ZipImportError: bad local file header'
zip_safe=False, zip_safe=False,

@ -0,0 +1,9 @@
@collection{Geometric_phases,
title = {Geometric phases in physics},
editor = {Shapere, Alfred and Wilczek, Frank},
year = {1989},
series = {Advanced Series in Mathematical Physics},
volume = {5},
publisher = {World Scientific},
isbn = {9789971506216}
}

@ -0,0 +1,7 @@
@software{hadoop,
author = {{Apache Software Foundation}},
title = {Hadoop},
url = {https://hadoop.apache.org},
version = {0.20.2},
date = {2010-02-19},
}

@ -9,7 +9,7 @@ import dotdot
from pyfakefs import fake_filesystem, fake_filesystem_unittest from pyfakefs import fake_filesystem, fake_filesystem_unittest
from pubs.p3 import input, _fake_stdio, _get_fake_stdio_ucontent from pubs.p3 import input
from pubs import content, filebroker, uis from pubs import content, filebroker, uis
# code for fake fs # code for fake fs
@ -29,29 +29,6 @@ original_exception_handler = uis.InputUI.handle_exception
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
# capture output
def capture(f, verbose=False):
"""Capture the stdout and stderr output.
Useful for comparing the output with the expected one during tests.
:param f: The function to capture output from.
:param verbose: If True, print call will still display their outputs.
If False, they will be silenced.
"""
def newf(*args, **kwargs):
old_stderr, old_stdout = sys.stderr, sys.stdout
sys.stdout = _fake_stdio(additional_out=old_stderr if verbose else None)
sys.stderr = _fake_stdio(additional_out=old_stderr if False else None)
try:
return f(*args, **kwargs), _get_fake_stdio_ucontent(sys.stdout), _get_fake_stdio_ucontent(sys.stderr)
finally:
sys.stderr, sys.stdout = old_stderr, old_stdout
return newf
# Test helpers # Test helpers
# automating input # automating input

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
bibtex_external0 = """ bibtex_external0 = r"""
@techreport{Page99, @techreport{Page99,
number = {1999-66}, number = {1999-66},
month = {November}, month = {November},
@ -17,7 +17,7 @@ institution = {Stanford InfoLab},
} }
""" """
bibtex_external_alt = """ bibtex_external_alt = r"""
@techreport{Page99, @techreport{Page99,
number = {1999-66}, number = {1999-66},
month = {November}, month = {November},
@ -33,7 +33,7 @@ institution = {Stanford InfoLab},
} }
""" """
bibtex_raw0 = """@techreport{ bibtex_raw0 = r"""@techreport{
Page99, Page99,
author = "Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry", author = "Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry",
publisher = "Stanford InfoLab", publisher = "Stanford InfoLab",
@ -50,12 +50,12 @@ bibtex_raw0 = """@techreport{
""" """
metadata_raw0 = """docfile: docsdir://Page99.pdf metadata_raw0 = r"""docfile: docsdir://Page99.pdf
tags: [search, network] tags: [search, network]
added: '2013-11-14 13:14:20' added: '2013-11-14 13:14:20'
""" """
turing_bib = """@article{turing1950computing, turing_bib = r"""@article{turing1950computing,
title={Computing machinery and intelligence}, title={Computing machinery and intelligence},
author={Turing, Alan M}, author={Turing, Alan M},
journal={Mind}, journal={Mind},
@ -75,7 +75,7 @@ added: '2013-11-14 13:14:20'
""" """
# Should not parse (see #113) # Should not parse (see #113)
bibtex_no_citekey = """@Manual{, bibtex_no_citekey = r"""@Manual{,
title = {R: A Language and Environment for Statistical Computing}, title = {R: A Language and Environment for Statistical Computing},
author = {{R Core Team}}, author = {{R Core Team}},
organization = {R Foundation for Statistical Computing}, organization = {R Foundation for Statistical Computing},
@ -85,7 +85,7 @@ bibtex_no_citekey = """@Manual{,
} }
""" """
bibtex_month = """@inproceedings{Goyal2017, bibtex_month = r"""@inproceedings{Goyal2017,
author = {Goyal, Anirudh and Sordoni, Alessandro and C{\^{o}}t{\'{e}}, Marc-Alexandre and Ke, Nan Rosemary and Bengio, Yoshua}, author = {Goyal, Anirudh and Sordoni, Alessandro and C{\^{o}}t{\'{e}}, Marc-Alexandre and Ke, Nan Rosemary and Bengio, Yoshua},
title = {Z-Forcing: Training Stochastic Recurrent Networks}, title = {Z-Forcing: Training Stochastic Recurrent Networks},
year = {2017}, year = {2017},
@ -94,15 +94,15 @@ bibtex_month = """@inproceedings{Goyal2017,
} }
""" """
not_bibtex = """@misc{this looks, not_bibtex = r"""@misc{this looks,
like = a = bibtex file but like = a = bibtex file but
, is not a real one! , is not a real one!
""" """
bibtex_with_latex = """@article{kjaer2018large, bibtex_with_latex = r"""@article{kjaer2018large,
title={A large impact crater beneath Hiawatha Glacier in northwest Greenland}, title={A large impact crater beneath Hiawatha Glacier in northwest Greenland},
author={Kj{\\ae}r, Kurt H and Larsen, Nicolaj K and Binder, Tobias and Bj{\\o}rk, Anders A and Eisen, Olaf and Fahnestock, Mark A and Funder, Svend and Garde, Adam A and Haack, Henning and Helm, Veit and others}, author={Kj{\ae}r, Kurt H and Larsen, Nicolaj K and Binder, Tobias and Bj{\o}rk, Anders A and Eisen, Olaf and Fahnestock, Mark A and Funder, Svend and Garde, Adam A and Haack, Henning and Helm, Veit and others},
journal={Science advances}, journal={Science advances},
volume={4}, volume={4},
number={11}, number={11},

@ -57,6 +57,12 @@ class TestEnDecode(unittest.TestCase):
self.assertEqual(bibraw1, bibraw2) self.assertEqual(bibraw1, bibraw2)
def test_decode_bibtex_preserves_type_field(self):
"""Test that multiple encode/decode step preserve data"""
decoder = endecoder.EnDecoder()
entry = decoder.decode_bibdata(bibtex_raw0)
self.assertEqual(entry['Page99']['type'], "Technical Report")
def test_endecode_bibtex_BOM(self): def test_endecode_bibtex_BOM(self):
"""Test that bibtexparser if fine with BOM-prefixed data""" """Test that bibtexparser if fine with BOM-prefixed data"""
decoder = endecoder.EnDecoder() decoder = endecoder.EnDecoder()

@ -71,6 +71,16 @@ class TestFileBroker(fake_env.TestFakeFs):
class TestDocBroker(fake_env.TestFakeFs): class TestDocBroker(fake_env.TestFakeFs):
def test_expanduser(self):
"""Test that real_docpath expand the user ~"""
self.fs.add_real_directory(os.path.join(self.rootpath, 'data'), read_only=False)
fb = filebroker.FileBroker('~/testrepo', create = True)
docb = filebroker.DocBroker('~/testrepo')
self.assertTrue(os.path.isabs(docb.docdir))
self.assertTrue(os.path.isabs(docb.real_docpath('docsdir://abc')))
def test_doccopy(self): def test_doccopy(self):
self.fs.add_real_directory(os.path.join(self.rootpath, 'data'), read_only=False) self.fs.add_real_directory(os.path.join(self.rootpath, 'data'), read_only=False)

@ -1,5 +1,6 @@
import unittest import os
import subprocess import subprocess
import unittest
import sand_env import sand_env
@ -16,11 +17,20 @@ class TestGitPlugin(sand_env.SandboxedCommandTestCase):
def setUp(self, nsec_stat=True): def setUp(self, nsec_stat=True):
super(TestGitPlugin, self).setUp() super(TestGitPlugin, self).setUp()
# Backup environment variables and set git author
self.env_backup = os.environ.copy()
os.environ['GIT_AUTHOR_NAME'] = "Pubs test"
os.environ['GIT_AUTHOR_EMAIL'] = "unittest@pubs.org"
# Setup pubs repository
self.execute_cmds([('pubs init',)]) self.execute_cmds([('pubs init',)])
conf = config.load_conf(path=self.default_conf_path) conf = config.load_conf(path=self.default_conf_path)
conf['plugins']['active'] = ['git'] conf['plugins']['active'] = ['git']
config.save_conf(conf, path=self.default_conf_path) config.save_conf(conf, path=self.default_conf_path)
def tearDown(self):
super().tearDown()
os.environ = self.env_backup
def test_git(self): def test_git(self):
self.execute_cmds([('pubs add data/pagerank.bib',)]) self.execute_cmds([('pubs add data/pagerank.bib',)])
hash_a = git_hash(self.default_pubs_dir) hash_a = git_hash(self.default_pubs_dir)
@ -31,7 +41,7 @@ class TestGitPlugin(sand_env.SandboxedCommandTestCase):
self.execute_cmds([('pubs rename Page99a ABC',)]) self.execute_cmds([('pubs rename Page99a ABC',)])
hash_c = git_hash(self.default_pubs_dir) hash_c = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs remove ABC', ['y']),]) self.execute_cmds([('pubs remove ABC', ['y'])])
hash_d = git_hash(self.default_pubs_dir) hash_d = git_hash(self.default_pubs_dir)
self.execute_cmds([('pubs doc add testrepo/doc/Page99.pdf Page99',)]) self.execute_cmds([('pubs doc add testrepo/doc/Page99.pdf Page99',)])
@ -72,6 +82,7 @@ class TestGitPlugin(sand_env.SandboxedCommandTestCase):
# self.assertEqual(hash_i, hash_j) # self.assertEqual(hash_i, hash_j)
def test_manual(self): def test_manual(self):
print(self.default_pubs_dir)
conf = config.load_conf(path=self.default_conf_path) conf = config.load_conf(path=self.default_conf_path)
conf['plugins']['active'] = ['git'] conf['plugins']['active'] = ['git']
conf['plugins']['git']['manual'] = True conf['plugins']['git']['manual'] = True
@ -101,6 +112,5 @@ class TestGitPlugin(sand_env.SandboxedCommandTestCase):
self.assertNotEqual(hash_l, hash_m) self.assertNotEqual(hash_l, hash_m)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

@ -46,7 +46,7 @@ class TestNoteAppend(DataCommandTestCase):
# * Pass the command split into a command and its args to # * Pass the command split into a command and its args to
# execute_cmdsplit, which is called by execute_cmds: # execute_cmdsplit, which is called by execute_cmds:
cmd_split = ['pubs', 'note', 'Page99', '-a', 'xxx yyy'] cmd_split = ['pubs', 'note', 'Page99', '-a', 'xxx yyy']
self.execute_cmdsplit(cmd_split, expected_out=None, expected_err=None) self.execute_cmd_capture(cmd_split, expected_out=None, expected_err=None)
note_lines.append('xxx yyy') note_lines.append('xxx yyy')
self.assertFileContentEqual(fin_notes, self._get_note_content(note_lines)) self.assertFileContentEqual(fin_notes, self._get_note_content(note_lines))

@ -1,7 +1,7 @@
import shlex import shlex
import unittest import unittest
import dotdot import dotdot
import argparse
import pubs import pubs
from pubs import config from pubs import config
@ -61,6 +61,19 @@ class AliasTestCase(unittest.TestCase):
shlex.split(self.subprocess.called.splitlines()[-1])[1:], shlex.split(self.subprocess.called.splitlines()[-1])[1:],
args) args)
def testShellAliasNamedArguments(self):
parser = argparse.ArgumentParser()
parser.add_argument('--test2')
subparsers = parser.add_subparsers(title='commands', dest='command')
alias = Alias.create_alias('test', '!echo "$@"')
alias.parser(subparsers)
args = ['test', '2', '--option', '3']
args = parser.parse_args(args)
self.assertEqual(args.command, 'test')
self.assertListEqual(args.arguments, ['2', '--option', '3'])
class AliasPluginTestCase(unittest.TestCase): class AliasPluginTestCase(unittest.TestCase):

@ -28,5 +28,15 @@ class TestPretty(unittest.TestCase):
line = '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)
def test_oneliner_max_authors(self):
decoder = endecoder.EnDecoder()
bibdata = decoder.decode_bibdata(bibtex_raw0)
for max_authors in [1, 2, 3]:
line = 'Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999)'
self.assertEqual(color.undye(pretty.bib_oneliner(bibdata['Page99'], max_authors=max_authors)), line)
for max_authors in [-1, 0, 4, 5, 10]:
line = 'Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry "The PageRank Citation Ranking: Bringing Order to the Web." (1999)'
self.assertEqual(color.undye(pretty.bib_oneliner(bibdata['Page99'], max_authors=max_authors)), line)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

@ -200,8 +200,8 @@ class TestFilterPaper(unittest.TestCase):
def test_latex_enc(self): def test_latex_enc(self):
latexenc_paper = doe_paper.deepcopy() latexenc_paper = doe_paper.deepcopy()
latexenc_paper.bibentry['Doe2013']['title'] = "{E}l Ni{\~n}o" latexenc_paper.bibentry['Doe2013']['title'] = r"{E}l Ni{\~n}o"
latexenc_paper.bibentry['Doe2013']['author'][0] = "Erd\H{o}s, Paul" latexenc_paper.bibentry['Doe2013']['author'][0] = r"Erd\H{o}s, Paul"
self.assertTrue(get_paper_filter(['title:El'])(latexenc_paper)) self.assertTrue(get_paper_filter(['title:El'])(latexenc_paper))
self.assertTrue(get_paper_filter(['title:Niño'])(latexenc_paper)) self.assertTrue(get_paper_filter(['title:Niño'])(latexenc_paper))
self.assertTrue(get_paper_filter(['author:erdős'])(latexenc_paper)) self.assertTrue(get_paper_filter(['author:erdős'])(latexenc_paper))
@ -209,12 +209,12 @@ class TestFilterPaper(unittest.TestCase):
def test_normalize_unicode(self): def test_normalize_unicode(self):
latexenc_paper = doe_paper.deepcopy() latexenc_paper = doe_paper.deepcopy()
latexenc_paper.bibentry['Doe2013']['title'] = "{E}l Ni{\~n}o" latexenc_paper.bibentry['Doe2013']['title'] = r"{E}l Ni{\~n}o"
self.assertTrue(get_paper_filter(['title:Nin\u0303o'])(latexenc_paper)) self.assertTrue(get_paper_filter(['title:Nin\u0303o'])(latexenc_paper))
def test_strict(self): def test_strict(self):
latexenc_paper = doe_paper.deepcopy() latexenc_paper = doe_paper.deepcopy()
latexenc_paper.bibentry['Doe2013']['title'] = "El Ni{\~n}o" latexenc_paper.bibentry['Doe2013']['title'] = r"El Ni{\~n}o"
self.assertFalse(get_paper_filter( self.assertFalse(get_paper_filter(
['title:Nin\u0303o'], strict=True)(latexenc_paper)) ['title:Nin\u0303o'], strict=True)(latexenc_paper))

@ -12,11 +12,13 @@ import ddt
import certifi import certifi
import mock import mock
from pyfakefs.fake_filesystem import FakeFileOpen from pyfakefs.fake_filesystem import FakeFileOpen
import pytest
import dotdot import dotdot
import fake_env import fake_env
import mock_requests import mock_requests
from pubs import pubs_cmd, color, content, uis, p3, endecoder from pubs import pubs_cmd, color, content, uis, p3, endecoder
from pubs.config import conf from pubs.config import conf
@ -25,7 +27,7 @@ import fixtures
# makes the tests very noisy # makes the tests very noisy
PRINT_OUTPUT = False PRINT_OUTPUT = True #False
CAPTURE_OUTPUT = True CAPTURE_OUTPUT = True
@ -118,14 +120,12 @@ class CommandTestCase(fake_env.TestFakeFs):
input.as_global() input.as_global()
try: try:
if capture_output: if capture_output:
actual_out = self.execute_cmdsplit( actual_out = self.execute_cmd_capture(actual_cmd.split(), expected_out, expected_err)
actual_cmd.split(), expected_out, 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:
self.fail('Unexpected input asked by command: {}.'.format( self.fail('Unexpected input asked by command: {}.'.format(actual_cmd))
actual_cmd))
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()
@ -145,27 +145,39 @@ class CommandTestCase(fake_env.TestFakeFs):
pass pass
return s return s
def execute_cmdsplit(self, actual_cmdlist, expected_out, expected_err): def execute_cmd_capture(self, cmd, expected_out, expected_err):
"""Run a single command, which has been split into a list containing cmd and args""" """Run a single command, captures the output and and stderr and compare it to the expected ones"""
capture_wrap = fake_env.capture(pubs_cmd.execute, sys_stdout, sys_stderr = sys.stdout, sys.stderr
verbose=PRINT_OUTPUT) sys.stdout = p3._fake_stdio(additional_out=sys_stdout if PRINT_OUTPUT else None)
_, stdout, stderr = capture_wrap(actual_cmdlist) sys.stderr = p3._fake_stdio(additional_out=sys_stderr if PRINT_OUTPUT else None)
actual_out = self.normalize(stdout)
actual_err = self.normalize(stderr) try:
pubs_cmd.execute(cmd)
finally:
# capturing output even if exception was raised.
self.captured_stdout = self.normalize(p3._get_fake_stdio_ucontent(sys.stdout))
self.captured_stderr = self.normalize(p3._get_fake_stdio_ucontent(sys.stderr))
sys.stderr, sys.stdout = sys_stderr, sys_stdout
if expected_out is not None: if expected_out is not None:
self.assertEqual(p3.u_maybe(actual_out), p3.u_maybe(expected_out)) self.assertEqual(p3.u_maybe(self.captured_stdout), p3.u_maybe(expected_out))
if expected_err is not None: if expected_err is not None:
self.assertEqual(p3.u_maybe(actual_err), p3.u_maybe(expected_err)) self.assertEqual(p3.u_maybe(self.captured_stderr), p3.u_maybe(expected_err))
return actual_out return self.captured_stdout
def update_config(self, config_update, path=None):
"""Allow to set the config parameters. Must have done a `pubs init` beforehand."""
if path is None:
path = self.default_conf_path
cfg = conf.load_conf(path=path)
for section, section_update in config_update.items():
cfg[section].update(section_update)
conf.save_conf(cfg, path=path)
def tearDown(self):
pass
class DataCommandTestCase(CommandTestCase): class DataCommandTestCase(CommandTestCase):
"""Abstract TestCase intializing the fake filesystem and """Abstract TestCase intializing the fake filesystem and copying fake data."""
copying fake data.
"""
def setUp(self, nsec_stat=True): def setUp(self, nsec_stat=True):
super(DataCommandTestCase, self).setUp(nsec_stat=nsec_stat) super(DataCommandTestCase, self).setUp(nsec_stat=nsec_stat)
@ -182,8 +194,7 @@ class DataCommandTestCase(CommandTestCase):
class URLContentTestCase(DataCommandTestCase): class URLContentTestCase(DataCommandTestCase):
"""Mocks access to online files by using files in data directory. """Mocks access to online files by using files in data directory."""
"""
def setUp(self): def setUp(self):
super(URLContentTestCase, self).setUp() super(URLContentTestCase, self).setUp()
@ -209,6 +220,8 @@ class URLContentTestCase(DataCommandTestCase):
content.url_exists = self._original_url_exist content.url_exists = self._original_url_exist
# Actual tests # Actual tests
class TestAlone(CommandTestCase): class TestAlone(CommandTestCase):
@ -615,6 +628,7 @@ class TestTag(DataCommandTestCase):
with self.assertRaises(FakeSystemExit): with self.assertRaises(FakeSystemExit):
self.execute_cmds(cmds) self.execute_cmds(cmds)
class TestURL(DataCommandTestCase): class TestURL(DataCommandTestCase):
def setUp(self): def setUp(self):
@ -720,7 +734,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',
'added to pubs:\n[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n' 'added to pubs:\n[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n'
'data/pagerank.pdf was copied to the pubs repository.\n', 'data/pagerank.pdf was copied to /paper_first/doc/Page99.pdf inside the pubs repository.\n',
'[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) [pdf] \n', '[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) [pdf] \n',
'\n', '\n',
'', '',
@ -1072,14 +1086,31 @@ class TestUsecase(DataCommandTestCase):
target_path=os.path.join('data', 'no-ext')) target_path=os.path.join('data', 'no-ext'))
correct = ['Initializing pubs in /pubs\n', correct = ['Initializing pubs in /pubs\n',
'added to pubs:\n[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n' 'added to pubs:\n[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n'
'data/no-ext was copied to the pubs repository.\n', 'data/no-ext was copied to /pubs/doc/Page99 inside the pubs repository.\n',
'[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) [NOEXT] \n', '[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) [NOEXT] \n',
] ]
cmds = ['pubs init -p /pubs', cmds = ['pubs init -p /pubs',
'pubs add -d data/no-ext data/pagerank.bib', 'pubs add -d data/no-ext data/pagerank.bib',
'pubs list', 'pubs list',
] ]
self.assertEqual(correct, self.execute_cmds(cmds, capture_output=True)) actual = self.execute_cmds(cmds, capture_output=True)
self.assertEqual(correct, actual)
def test_add_non_standard(self):
"""Test that non-standard bibtex are correctly added"""
self.fs.add_real_directory(os.path.join(self.rootpath, 'data_non_standard'), read_only=False)
correct = ['Initializing pubs in /pubs\n',
'added to pubs:\n[Geometric_phases] "Geometric phases in physics" (1989) \n',
'added to pubs:\n[hadoop] Foundation, Apache Software "Hadoop" \n',
]
cmds = ['pubs init -p /pubs',
'pubs add data_non_standard/non_standard_collection.bib',
'pubs add data_non_standard/non_standard_software.bib',
# 'pubs list',
]
actual = self.execute_cmds(cmds, capture_output=True)
self.assertEqual(correct, actual)
@mock.patch('pubs.apis.requests.get', side_effect=mock_requests.mock_requests_get) @mock.patch('pubs.apis.requests.get', side_effect=mock_requests.mock_requests_get)
def test_readme(self, reqget): def test_readme(self, reqget):
@ -1089,7 +1120,7 @@ class TestUsecase(DataCommandTestCase):
self.fs.add_real_file(os.path.join(self.rootpath, 'data/pagerank.pdf'), target_path='data/Knuth1995.pdf') self.fs.add_real_file(os.path.join(self.rootpath, 'data/pagerank.pdf'), target_path='data/Knuth1995.pdf')
cmds = ['pubs init', cmds = ['pubs init',
'pubs import data/collection.bib', 'pubs import data/three_articles.bib',
'pubs add data/pagerank.bib -d data/pagerank.pdf', 'pubs add data/pagerank.bib -d data/pagerank.pdf',
#'pubs add -D 10.1007/s00422-012-0514-6 -d data/pagerank.pdf', #'pubs add -D 10.1007/s00422-012-0514-6 -d data/pagerank.pdf',
'pubs add -I 978-0822324669 -d data/oyama2000the.pdf', 'pubs add -I 978-0822324669 -d data/oyama2000the.pdf',
@ -1100,6 +1131,21 @@ class TestUsecase(DataCommandTestCase):
self.execute_cmds(cmds, capture_output=True) self.execute_cmds(cmds, capture_output=True)
# self.assertEqual(correct, self.execute_cmds(cmds, capture_output=True)) # self.assertEqual(correct, self.execute_cmds(cmds, capture_output=True))
def test_ambiguous_citekey(self):
cmds = ['pubs init',
'pubs add data/pagerank.bib',
'pubs add data/pagerank.bib', # now we have Page99 and Page99a
'pubs edit Page',
]
output = '\n'.join(["error: Be more specific; 'Page' matches multiples citekeys:",
" [Page99] Page, Lawrence et al. \"The PageRank Citation Ranking: Bringing Order to the Web.\" (1999) ",
" [Page99a] Page, Lawrence et al. \"The PageRank Citation Ranking: Bringing Order to the Web.\" (1999) \n"])
with self.assertRaises(FakeSystemExit):
self.execute_cmds(cmds)
self.assertEqual(self.captured_stderr, output)
@ddt.ddt @ddt.ddt
@ -1145,5 +1191,23 @@ class TestCache(DataCommandTestCase):
self.assertEqual(line1, out[4]) self.assertEqual(line1, out[4])
class TestConfigChange(DataCommandTestCase):
def test_max_authors_default(self):
line_al = '[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n'
line_full = '[Page99] Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n'
self.execute_cmds(['pubs init', 'pubs add data/pagerank.bib'])
for max_authors in [1, 2, 3]:
self.update_config({'main': {'max_authors': max_authors}})
self.execute_cmds([('pubs list', None, line_al, None)])
for max_authors in [-1, 0, 4, 5, 10]:
self.update_config({'main': {'max_authors': max_authors}})
self.execute_cmds([('pubs list', None, line_full, None)])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main(verbosity=2) unittest.main(verbosity=2)

Loading…
Cancel
Save