papers-cli 2.17.1__tar.gz → 2.17.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {papers_cli-2.17.1 → papers_cli-2.17.3}/PKG-INFO +1 -1
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/installation.md +17 -4
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/__main__.py +63 -8
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/_version.py +3 -3
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/utils.py +36 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/PKG-INFO +1 -1
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/SOURCES.txt +2 -0
- papers_cli-2.17.3/papers_cli.egg-info/scm_file_list.json +71 -0
- papers_cli-2.17.3/papers_cli.egg-info/scm_version.json +8 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_add.py +16 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_list.py +114 -1
- {papers_cli-2.17.1 → papers_cli-2.17.3}/.gitattributes +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/FUNDING.yml +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/workflows/docs.yml +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/workflows/pypi.yml +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/workflows/release.yml +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/workflows/tests.yml +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/.gitignore +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/LICENSE +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/README.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/adding-entries.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/comparison.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/configuration.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/features.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/git.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/index.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/library-management.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/listing.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/quickstart.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/renaming.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/undo-redo.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/mkdocs.yml +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/__init__.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/backup.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/bib.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/config.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/duplicate.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/encoding.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/entries.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/extract.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/filename.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/install.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/latexenc.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/dependency_links.txt +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/entry_points.txt +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/requires.txt +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/top_level.txt +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/pyproject.toml +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/requirements.txt +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/scripts/README.md +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/scripts/benchmark_bibtexparser_load.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/scripts/generate_dummy_bib.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/setup.cfg +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/setup.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/__init__.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/common.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/config.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/download.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/.gitignore +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/bg-8-515-2011.pdf +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/esd-4-11-2013-supplement.pdf +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/esd-4-11-2013.pdf +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_backup_dirs.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_bib_unit.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_biblio.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_check.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_config_unit.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_duplicates.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_encoding.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_extract.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_extract_unit.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_filecheck.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_filename_unit.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_install.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_latexenc.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_undo.py +0 -0
- {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_utils.py +0 -0
|
@@ -14,10 +14,23 @@ Note there is another project registered on PyPI as `papers`, hence
|
|
|
14
14
|
|
|
15
15
|
The [GNOME Papers](https://gitlab.gnome.org/GNOME/papers) document viewer
|
|
16
16
|
(the default viewer on recent GNOME desktops) also installs a `papers`
|
|
17
|
-
command; with both installed, whichever comes first in `$PATH` wins.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
command; with both installed, whichever comes first in `$PATH` wins. Two
|
|
18
|
+
mitigations are built in:
|
|
19
|
+
|
|
20
|
+
- The package also installs the command under the unambiguous name
|
|
21
|
+
`papers-cli` — call that, or set a shell alias (e.g.
|
|
22
|
+
`alias papers=papers-cli`), on systems where the names collide.
|
|
23
|
+
- Because this command may mask GNOME Papers, a call whose arguments are all
|
|
24
|
+
existing files (paths or `file://` URIs, as passed by file managers through
|
|
25
|
+
the viewer's desktop entry) is handed over unchanged to the next `papers`
|
|
26
|
+
executable in `$PATH` that is not this tool, i.e. GNOME Papers itself.
|
|
27
|
+
Double-clicking a PDF and `papers somefile.pdf` therefore keep opening the
|
|
28
|
+
viewer. If no other `papers` is found, the files are opened with the
|
|
29
|
+
system's default viewer (`xdg-open`) instead; should that lead back to
|
|
30
|
+
this command, it stops with an error rather than looping. Subcommand names
|
|
31
|
+
always take precedence (`papers ./list` opens a file named `list`;
|
|
32
|
+
`papers list` lists your library), and any flag disables the hand-over —
|
|
33
|
+
call the viewer's binary directly for viewer-specific flags.
|
|
21
34
|
|
|
22
35
|
## Dependencies
|
|
23
36
|
|
|
@@ -24,7 +24,7 @@ from papers.bib import (Biblio, FUZZY_RATIO, DEFAULT_SIMILARITY, entry_filecheck
|
|
|
24
24
|
backupfile as backupfile_func, isvalidkey, DuplicateKeyError, clean_filesdir,
|
|
25
25
|
are_duplicates, download_url, get_biblio)
|
|
26
26
|
from papers.install import resolve_install, apply_install, InputAsker, DefaultAsker
|
|
27
|
-
from papers.utils import view_pdf, open_folder, PapersExit
|
|
27
|
+
from papers.utils import view_pdf, open_folder, PapersExit, file_uri_to_path, find_masked_viewer
|
|
28
28
|
from papers.backup import (silent_backup_bib, restore_from_backupdir,
|
|
29
29
|
git_undo, git_redo, git_restore_state, list_backup_dirs)
|
|
30
30
|
from papers import __version__
|
|
@@ -731,9 +731,21 @@ def listcmd(parser, o, config):
|
|
|
731
731
|
|
|
732
732
|
|
|
733
733
|
def opencmd(parser, o, config):
|
|
734
|
-
biblio =
|
|
735
|
-
entries_by_key = {
|
|
734
|
+
biblio = None
|
|
735
|
+
entries_by_key = {}
|
|
736
736
|
for key in o.key:
|
|
737
|
+
# an existing file is opened directly with the system viewer; anything
|
|
738
|
+
# else is looked up as an entry key in the bibliography
|
|
739
|
+
if os.path.isfile(key):
|
|
740
|
+
logger.info(f"opening {key} ...")
|
|
741
|
+
view_pdf(key)
|
|
742
|
+
continue
|
|
743
|
+
if biblio is None:
|
|
744
|
+
try:
|
|
745
|
+
biblio = get_biblio(config)
|
|
746
|
+
except ValueError:
|
|
747
|
+
raise PapersExit(f"'{key}' is not an existing file, and no bibliography is configured to look it up as a key")
|
|
748
|
+
entries_by_key = {biblio.key(e): e for e in biblio.entries}
|
|
737
749
|
e = entries_by_key.get(key.lower())
|
|
738
750
|
if e is None:
|
|
739
751
|
logger.error(f'no entry found with key: {key}')
|
|
@@ -782,7 +794,10 @@ def get_parser(config=None):
|
|
|
782
794
|
keyfmt = argparse.ArgumentParser(add_help=False)
|
|
783
795
|
grp = keyfmt.add_argument_group('bibtex key format')
|
|
784
796
|
grp.add_argument('--key-template', default=config.keyformat.template,
|
|
785
|
-
help='python template for generating keys (default:%(default)s)
|
|
797
|
+
help='''python template for generating keys (default:%(default)s).
|
|
798
|
+
The template applies to entries generated from a PDF or a DOI;
|
|
799
|
+
entries added from a bibtex file keep their original key unless
|
|
800
|
+
-u/--update-key is passed.''')
|
|
786
801
|
grp.add_argument('--key-author-num', type=int, default=config.keyformat.author_num,
|
|
787
802
|
help='number of authors to include in key (default:%(default)s)')
|
|
788
803
|
grp.add_argument('--key-author-sep', default=config.keyformat.author_sep,
|
|
@@ -872,7 +887,10 @@ def get_parser(config=None):
|
|
|
872
887
|
grp.add_argument('--no-merge-files', action='store_true',
|
|
873
888
|
help='distinct "file" field considered a conflict, all other things being equal')
|
|
874
889
|
grp.add_argument('-u', '--update-key', action='store_true',
|
|
875
|
-
help='
|
|
890
|
+
help='''regenerate the key of added entries from the key template
|
|
891
|
+
(see "bibtex key format" options); if a duplicate is found, use the
|
|
892
|
+
existing entry's key instead (otherwise an error might be raised on
|
|
893
|
+
identical insert key)''')
|
|
876
894
|
# grp.add_argument('-f', '--force', action='store_true', help='no interactive')
|
|
877
895
|
grp.add_argument('-m', '--mode', default='i', choices=['u', 'U', 'o', 's', 'r', 'i', 'a'],
|
|
878
896
|
help='''if duplicates are found, the default is to start an (i)nteractive dialogue,
|
|
@@ -1040,8 +1058,8 @@ def get_parser(config=None):
|
|
|
1040
1058
|
|
|
1041
1059
|
# open
|
|
1042
1060
|
# ====
|
|
1043
|
-
openp = subparsers.add_parser('open', description='open
|
|
1044
|
-
openp.add_argument('key', nargs='+', help='entry key(s)
|
|
1061
|
+
openp = subparsers.add_parser('open', description='open entry attachments or files with the system viewer', parents=[cfg])
|
|
1062
|
+
openp.add_argument('key', nargs='+', help='entry key(s) (case-insensitive), or existing file path(s) to open directly')
|
|
1045
1063
|
|
|
1046
1064
|
# doi
|
|
1047
1065
|
# ===
|
|
@@ -1107,6 +1125,10 @@ def handle_logging(o):
|
|
|
1107
1125
|
logger.debug("LOGGER LEVEL: "+logging.getLevelName(logger.getEffectiveLevel()))
|
|
1108
1126
|
|
|
1109
1127
|
|
|
1128
|
+
# set while handing files to the system viewer, to detect a passthrough loop (#107)
|
|
1129
|
+
VIEWER_PASSTHROUGH_ENV = 'PAPERS_CLI_VIEWER_PASSTHROUGH'
|
|
1130
|
+
|
|
1131
|
+
|
|
1110
1132
|
def main(args=None):
|
|
1111
1133
|
papers.config.DRYRUN = False # reset in case main() if called directly
|
|
1112
1134
|
if args is not None:
|
|
@@ -1125,6 +1147,37 @@ def main(args=None):
|
|
|
1125
1147
|
|
|
1126
1148
|
parser, subparsers = get_parser(config)
|
|
1127
1149
|
|
|
1150
|
+
# viewer passthrough (#107): this command often masks the GNOME Papers
|
|
1151
|
+
# document viewer in $PATH. Its desktop entry runs `papers %U`, so file
|
|
1152
|
+
# managers and xdg-open land here with file paths or file:// URIs. When
|
|
1153
|
+
# every argument is an existing file, the call is handed over unchanged
|
|
1154
|
+
# to the masked viewer (the next `papers` on $PATH that is not this tool).
|
|
1155
|
+
# xdg-open is only a fallback when there is no such viewer: it resolves
|
|
1156
|
+
# the desktop entry, which may lead back to this command, hence the
|
|
1157
|
+
# environment guard against a loop. Subcommand names always take
|
|
1158
|
+
# precedence, and any flag disables the passthrough (it cannot be meant
|
|
1159
|
+
# for this tool).
|
|
1160
|
+
argv = sys.argv[1:]
|
|
1161
|
+
if argv and all(not a.startswith('-') for a in argv) \
|
|
1162
|
+
and argv[0] not in subparsers.choices \
|
|
1163
|
+
and all(os.path.isfile(file_uri_to_path(a)) for a in argv):
|
|
1164
|
+
viewer = find_masked_viewer()
|
|
1165
|
+
if viewer:
|
|
1166
|
+
print(f"papers-cli: passing {' '.join(map(repr, argv))} to {viewer} "
|
|
1167
|
+
f"(see `papers --help` for the bibliography tool)", file=sys.stderr)
|
|
1168
|
+
os.execv(viewer, [viewer] + argv)
|
|
1169
|
+
return # only reached when os.execv is mocked
|
|
1170
|
+
if os.environ.get(VIEWER_PASSTHROUGH_ENV):
|
|
1171
|
+
raise PapersExit("the system viewer for these files leads back to this "
|
|
1172
|
+
"command, and no other `papers` viewer is in $PATH")
|
|
1173
|
+
os.environ[VIEWER_PASSTHROUGH_ENV] = '1' # inherited by the viewer
|
|
1174
|
+
for a in argv:
|
|
1175
|
+
f = file_uri_to_path(a)
|
|
1176
|
+
print(f"papers-cli: opening {f!r} with the system viewer "
|
|
1177
|
+
f"(see `papers --help` for the bibliography tool)", file=sys.stderr)
|
|
1178
|
+
view_pdf(f)
|
|
1179
|
+
return
|
|
1180
|
+
|
|
1128
1181
|
o, args = parser.parse_known_args(args)
|
|
1129
1182
|
|
|
1130
1183
|
if o.version:
|
|
@@ -1167,7 +1220,9 @@ def main(args=None):
|
|
|
1167
1220
|
elif o.cmd == 'list':
|
|
1168
1221
|
check_install(subp, o, config) and listcmd(subp, o, config)
|
|
1169
1222
|
elif o.cmd == 'open':
|
|
1170
|
-
|
|
1223
|
+
# no install required when opening plain files; opencmd reports the
|
|
1224
|
+
# missing bibliography itself when an argument must be looked up as a key
|
|
1225
|
+
opencmd(subp, o, config)
|
|
1171
1226
|
elif o.cmd == 'undo':
|
|
1172
1227
|
check_install(subp, o, config) and undocmd(subp, o, config)
|
|
1173
1228
|
elif o.cmd == 'redo':
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.17.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 17,
|
|
21
|
+
__version__ = version = '2.17.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 17, 3)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g7999cf082'
|
|
@@ -203,6 +203,42 @@ def view_pdf(filepath):
|
|
|
203
203
|
subprocess.call(('xdg-open', filepath))
|
|
204
204
|
|
|
205
205
|
|
|
206
|
+
def file_uri_to_path(arg):
|
|
207
|
+
"""Return the local path of a file:// URI, or `arg` unchanged otherwise
|
|
208
|
+
|
|
209
|
+
Desktop entries launch viewers with `%U`, i.e. with file:// URIs.
|
|
210
|
+
"""
|
|
211
|
+
if arg.startswith('file://'):
|
|
212
|
+
from urllib.parse import urlparse, unquote
|
|
213
|
+
return unquote(urlparse(arg).path)
|
|
214
|
+
return arg
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _is_papers_cli_script(path):
|
|
218
|
+
"""True if `path` is a console script of this package (`papers` or `papers-cli`)"""
|
|
219
|
+
try:
|
|
220
|
+
with open(path, 'rb') as f:
|
|
221
|
+
head = f.read(1024)
|
|
222
|
+
except OSError:
|
|
223
|
+
return False
|
|
224
|
+
return b'papers.__main__' in head
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def find_masked_viewer(name='papers'):
|
|
228
|
+
"""Return the first `name` executable on $PATH that is not this tool, or None
|
|
229
|
+
|
|
230
|
+
This tool's `papers` command often masks the GNOME Papers document viewer
|
|
231
|
+
in $PATH (issue #107). Console scripts of this package are recognized by
|
|
232
|
+
their content, so other installs of it (pipx, venvs) are skipped too.
|
|
233
|
+
"""
|
|
234
|
+
for d in os.environ.get('PATH', '').split(os.pathsep):
|
|
235
|
+
candidate = os.path.join(d or '.', name)
|
|
236
|
+
if os.path.isfile(candidate) and os.access(candidate, os.X_OK) \
|
|
237
|
+
and not _is_papers_cli_script(candidate):
|
|
238
|
+
return candidate
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
|
|
206
242
|
def open_folder(path):
|
|
207
243
|
system_platform = platform.system()
|
|
208
244
|
|
|
@@ -42,6 +42,8 @@ papers_cli.egg-info/SOURCES.txt
|
|
|
42
42
|
papers_cli.egg-info/dependency_links.txt
|
|
43
43
|
papers_cli.egg-info/entry_points.txt
|
|
44
44
|
papers_cli.egg-info/requires.txt
|
|
45
|
+
papers_cli.egg-info/scm_file_list.json
|
|
46
|
+
papers_cli.egg-info/scm_version.json
|
|
45
47
|
papers_cli.egg-info/top_level.txt
|
|
46
48
|
scripts/README.md
|
|
47
49
|
scripts/benchmark_bibtexparser_load.py
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".gitattributes",
|
|
4
|
+
".github/FUNDING.yml",
|
|
5
|
+
".github/workflows/docs.yml",
|
|
6
|
+
".github/workflows/pypi.yml",
|
|
7
|
+
".github/workflows/release.yml",
|
|
8
|
+
".github/workflows/tests.yml",
|
|
9
|
+
".gitignore",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md",
|
|
12
|
+
"docs/adding-entries.md",
|
|
13
|
+
"docs/comparison.md",
|
|
14
|
+
"docs/configuration.md",
|
|
15
|
+
"docs/features.md",
|
|
16
|
+
"docs/git.md",
|
|
17
|
+
"docs/index.md",
|
|
18
|
+
"docs/installation.md",
|
|
19
|
+
"docs/library-management.md",
|
|
20
|
+
"docs/listing.md",
|
|
21
|
+
"docs/quickstart.md",
|
|
22
|
+
"docs/renaming.md",
|
|
23
|
+
"docs/undo-redo.md",
|
|
24
|
+
"mkdocs.yml",
|
|
25
|
+
"papers/__init__.py",
|
|
26
|
+
"papers/__main__.py",
|
|
27
|
+
"papers/backup.py",
|
|
28
|
+
"papers/bib.py",
|
|
29
|
+
"papers/config.py",
|
|
30
|
+
"papers/duplicate.py",
|
|
31
|
+
"papers/encoding.py",
|
|
32
|
+
"papers/entries.py",
|
|
33
|
+
"papers/extract.py",
|
|
34
|
+
"papers/filename.py",
|
|
35
|
+
"papers/install.py",
|
|
36
|
+
"papers/latexenc.py",
|
|
37
|
+
"papers/utils.py",
|
|
38
|
+
"pyproject.toml",
|
|
39
|
+
"requirements.txt",
|
|
40
|
+
"scripts/README.md",
|
|
41
|
+
"scripts/benchmark_bibtexparser_load.py",
|
|
42
|
+
"scripts/generate_dummy_bib.py",
|
|
43
|
+
"setup.py",
|
|
44
|
+
"tests/__init__.py",
|
|
45
|
+
"tests/common.py",
|
|
46
|
+
"tests/config.py",
|
|
47
|
+
"tests/download.py",
|
|
48
|
+
"tests/downloadedpapers/.gitignore",
|
|
49
|
+
"tests/downloadedpapers/bg-8-515-2011.pdf",
|
|
50
|
+
"tests/downloadedpapers/bg-8-515-2011.txt",
|
|
51
|
+
"tests/downloadedpapers/esd-4-11-2013-supplement.pdf",
|
|
52
|
+
"tests/downloadedpapers/esd-4-11-2013.pdf",
|
|
53
|
+
"tests/test_add.py",
|
|
54
|
+
"tests/test_backup_dirs.py",
|
|
55
|
+
"tests/test_bib_unit.py",
|
|
56
|
+
"tests/test_biblio.py",
|
|
57
|
+
"tests/test_check.py",
|
|
58
|
+
"tests/test_config_unit.py",
|
|
59
|
+
"tests/test_duplicates.py",
|
|
60
|
+
"tests/test_encoding.py",
|
|
61
|
+
"tests/test_extract.py",
|
|
62
|
+
"tests/test_extract_unit.py",
|
|
63
|
+
"tests/test_filecheck.py",
|
|
64
|
+
"tests/test_filename_unit.py",
|
|
65
|
+
"tests/test_install.py",
|
|
66
|
+
"tests/test_latexenc.py",
|
|
67
|
+
"tests/test_list.py",
|
|
68
|
+
"tests/test_undo.py",
|
|
69
|
+
"tests/test_utils.py"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -196,6 +196,22 @@ class TestAddBib(BibTest):
|
|
|
196
196
|
self.assertEqual(bib.db.entries[0]['ID'], self.key1)
|
|
197
197
|
self.assertTrue(self.key2 not in [e['ID'] for e in self.my.db.entries])
|
|
198
198
|
|
|
199
|
+
def test_addbib_cmd_update_key(self):
|
|
200
|
+
# --update-key regenerates the key of entries added from a bibtex file
|
|
201
|
+
# according to the key template (issue #108)
|
|
202
|
+
paperscmd(f'add {self.somebib} --bibtex {self.mybib} --update-key')
|
|
203
|
+
bib = Biblio.load(self.mybib, '')
|
|
204
|
+
self.assertEqual(len(bib.db.entries), 2)
|
|
205
|
+
self.assertEqual(bib.db.entries[0]['ID'], self.key1) # pre-existing entry untouched
|
|
206
|
+
self.assertEqual(bib.db.entries[1]['ID'], self.newkey2) # default template {author}{year}
|
|
207
|
+
|
|
208
|
+
def test_addbib_cmd_update_key_custom_template(self):
|
|
209
|
+
paperscmd(f'add {self.somebib} --bibtex {self.mybib} -u --key-template "{{year}}"')
|
|
210
|
+
bib = Biblio.load(self.mybib, '')
|
|
211
|
+
self.assertEqual(len(bib.db.entries), 2)
|
|
212
|
+
self.assertEqual(bib.db.entries[0]['ID'], self.key1)
|
|
213
|
+
self.assertEqual(bib.db.entries[1]['ID'], self.year)
|
|
214
|
+
|
|
199
215
|
def test_attachment_fails_with_multiple_entries(self):
|
|
200
216
|
func = lambda: paperscmd(f'add {self.pdf} {self.pdf} --bibtex {self.mybib} --filesdir {self.filesdir} --attachment {self.pdf}')
|
|
201
217
|
self.assertRaises(Exception, func)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import bibtexparser
|
|
2
|
-
|
|
2
|
+
import os
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
from tests.common import LocalInstallTest, BaseTest, Biblio, tempfile
|
|
7
|
+
from papers.__main__ import VIEWER_PASSTHROUGH_ENV
|
|
8
|
+
from papers.utils import PapersExit, find_masked_viewer, file_uri_to_path
|
|
3
9
|
from papers.utils import strip_all
|
|
4
10
|
|
|
5
11
|
bibtex = """@article{Perrette_2011,
|
|
@@ -246,3 +252,110 @@ class OpenCmdTest(LocalInstallTest):
|
|
|
246
252
|
|
|
247
253
|
def test_open_unknown_key(self):
|
|
248
254
|
self.papers('open NoSuchKey') # logs an error, does not crash
|
|
255
|
+
|
|
256
|
+
def test_open_file_path(self):
|
|
257
|
+
# `papers open` also accepts existing file paths directly
|
|
258
|
+
from unittest.mock import patch
|
|
259
|
+
f = self._path('direct.pdf')
|
|
260
|
+
open(f, 'w').write('x')
|
|
261
|
+
with patch('papers.__main__.view_pdf') as viewer:
|
|
262
|
+
self.papers(f'open {f}')
|
|
263
|
+
viewer.assert_called_once_with(f)
|
|
264
|
+
|
|
265
|
+
def test_bare_file_argument_opens_viewer(self):
|
|
266
|
+
# `papers somefile.pdf` behaves like the (possibly masked) document
|
|
267
|
+
# viewer: without another `papers` in $PATH, the file is opened with
|
|
268
|
+
# the system viewer (issue #107)
|
|
269
|
+
f = self._path('direct.pdf')
|
|
270
|
+
open(f, 'w').write('x')
|
|
271
|
+
with patch.dict(os.environ), \
|
|
272
|
+
patch('papers.__main__.find_masked_viewer', return_value=None), \
|
|
273
|
+
patch('papers.__main__.view_pdf') as viewer:
|
|
274
|
+
os.environ.pop(VIEWER_PASSTHROUGH_ENV, None)
|
|
275
|
+
self.papers(f'{f}')
|
|
276
|
+
viewer.assert_called_once_with(f)
|
|
277
|
+
|
|
278
|
+
def test_subcommand_wins_over_file(self):
|
|
279
|
+
# a file named like a subcommand does not hijack the CLI
|
|
280
|
+
open(self._path('status'), 'w').write('x')
|
|
281
|
+
with patch('papers.__main__.find_masked_viewer') as finder, \
|
|
282
|
+
patch('papers.__main__.view_pdf') as viewer:
|
|
283
|
+
self.papers('status')
|
|
284
|
+
finder.assert_not_called()
|
|
285
|
+
viewer.assert_not_called()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ViewerPassthroughNoInstallTest(BaseTest):
|
|
289
|
+
anotherbib_content = None
|
|
290
|
+
|
|
291
|
+
def setUp(self):
|
|
292
|
+
super().setUp()
|
|
293
|
+
self.pdf = self._path('doc.pdf')
|
|
294
|
+
open(self.pdf, 'w').write('x')
|
|
295
|
+
env = patch.dict(os.environ)
|
|
296
|
+
env.start()
|
|
297
|
+
self.addCleanup(env.stop)
|
|
298
|
+
os.environ.pop(VIEWER_PASSTHROUGH_ENV, None)
|
|
299
|
+
|
|
300
|
+
def test_bare_file_argument_without_install(self):
|
|
301
|
+
# the viewer passthrough must work without any papers install
|
|
302
|
+
with patch('papers.__main__.find_masked_viewer', return_value=None), \
|
|
303
|
+
patch('papers.__main__.view_pdf') as viewer:
|
|
304
|
+
self.papers(f'{self.pdf}')
|
|
305
|
+
viewer.assert_called_once_with(self.pdf)
|
|
306
|
+
|
|
307
|
+
def test_file_uri_argument(self):
|
|
308
|
+
# desktop entries launch the viewer as `papers %U`, i.e. with file:// URIs
|
|
309
|
+
uri = 'file://' + quote(self.pdf)
|
|
310
|
+
with patch('papers.__main__.find_masked_viewer', return_value=None), \
|
|
311
|
+
patch('papers.__main__.view_pdf') as viewer:
|
|
312
|
+
self.papers(f"'{uri}'")
|
|
313
|
+
viewer.assert_called_once_with(self.pdf)
|
|
314
|
+
|
|
315
|
+
def test_masked_viewer_receives_arguments_unchanged(self):
|
|
316
|
+
# the masked viewer is exec'd with the original arguments, instead of
|
|
317
|
+
# xdg-open, which may resolve back to this command
|
|
318
|
+
uri = 'file://' + quote(self.pdf)
|
|
319
|
+
with patch('papers.__main__.find_masked_viewer', return_value='/usr/bin/papers'), \
|
|
320
|
+
patch('papers.__main__.os.execv') as execv, \
|
|
321
|
+
patch('papers.__main__.view_pdf') as viewer:
|
|
322
|
+
self.papers(f"'{uri}' {self.pdf}")
|
|
323
|
+
execv.assert_called_once_with('/usr/bin/papers', ['/usr/bin/papers', uri, self.pdf])
|
|
324
|
+
viewer.assert_not_called()
|
|
325
|
+
|
|
326
|
+
def test_loop_guard(self):
|
|
327
|
+
# handed to the system viewer once already: stop instead of looping
|
|
328
|
+
os.environ[VIEWER_PASSTHROUGH_ENV] = '1'
|
|
329
|
+
with patch('papers.__main__.find_masked_viewer', return_value=None), \
|
|
330
|
+
patch('papers.__main__.view_pdf') as viewer:
|
|
331
|
+
with self.assertRaises(PapersExit):
|
|
332
|
+
self.papers(f'{self.pdf}')
|
|
333
|
+
viewer.assert_not_called()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class FindMaskedViewerTest(unittest.TestCase):
|
|
337
|
+
|
|
338
|
+
def _executable(self, path, content):
|
|
339
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
340
|
+
open(path, 'w').write(content)
|
|
341
|
+
os.chmod(path, 0o755)
|
|
342
|
+
|
|
343
|
+
def test_skips_papers_cli_scripts(self):
|
|
344
|
+
with tempfile.TemporaryDirectory() as d:
|
|
345
|
+
script = '#!/usr/bin/python3\nfrom papers.__main__ import main_clean_exit\n'
|
|
346
|
+
self._executable(os.path.join(d, 'venv', 'papers'), script)
|
|
347
|
+
self._executable(os.path.join(d, 'pipx', 'papers'), script)
|
|
348
|
+
self._executable(os.path.join(d, 'usr', 'papers'), '#!/bin/sh\n')
|
|
349
|
+
path = os.pathsep.join(os.path.join(d, x) for x in ['venv', 'empty', 'pipx', 'usr'])
|
|
350
|
+
with patch.dict(os.environ, {'PATH': path}):
|
|
351
|
+
self.assertEqual(find_masked_viewer(), os.path.join(d, 'usr', 'papers'))
|
|
352
|
+
|
|
353
|
+
def test_none_without_other_viewer(self):
|
|
354
|
+
with tempfile.TemporaryDirectory() as d:
|
|
355
|
+
self._executable(os.path.join(d, 'papers'), 'from papers.__main__ import main\n')
|
|
356
|
+
with patch.dict(os.environ, {'PATH': d}):
|
|
357
|
+
self.assertIsNone(find_masked_viewer())
|
|
358
|
+
|
|
359
|
+
def test_file_uri_to_path(self):
|
|
360
|
+
self.assertEqual(file_uri_to_path('file:///tmp/a%20b,%203.pdf'), '/tmp/a b, 3.pdf')
|
|
361
|
+
self.assertEqual(file_uri_to_path('/tmp/a b.pdf'), '/tmp/a b.pdf')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|