papers-cli 2.17.2__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.2 → papers_cli-2.17.3}/PKG-INFO +1 -1
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/installation.md +11 -7
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/__main__.py +35 -10
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/_version.py +3 -3
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/utils.py +36 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers_cli.egg-info/PKG-INFO +1 -1
- {papers_cli-2.17.2 → 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.2 → papers_cli-2.17.3}/tests/test_add.py +16 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_list.py +84 -11
- {papers_cli-2.17.2 → papers_cli-2.17.3}/.gitattributes +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/.github/FUNDING.yml +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/.github/workflows/docs.yml +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/.github/workflows/pypi.yml +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/.github/workflows/release.yml +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/.github/workflows/tests.yml +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/.gitignore +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/LICENSE +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/README.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/adding-entries.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/comparison.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/configuration.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/features.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/git.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/index.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/library-management.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/listing.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/quickstart.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/renaming.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/docs/undo-redo.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/mkdocs.yml +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/__init__.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/backup.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/bib.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/config.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/duplicate.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/encoding.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/entries.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/extract.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/filename.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/install.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers/latexenc.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers_cli.egg-info/dependency_links.txt +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers_cli.egg-info/entry_points.txt +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers_cli.egg-info/requires.txt +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/papers_cli.egg-info/top_level.txt +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/pyproject.toml +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/requirements.txt +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/scripts/README.md +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/scripts/benchmark_bibtexparser_load.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/scripts/generate_dummy_bib.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/setup.cfg +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/setup.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/__init__.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/common.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/config.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/download.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/downloadedpapers/.gitignore +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/downloadedpapers/bg-8-515-2011.pdf +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/downloadedpapers/esd-4-11-2013-supplement.pdf +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/downloadedpapers/esd-4-11-2013.pdf +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_backup_dirs.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_bib_unit.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_biblio.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_check.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_config_unit.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_duplicates.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_encoding.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_extract.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_extract_unit.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_filecheck.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_filename_unit.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_install.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_latexenc.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_undo.py +0 -0
- {papers_cli-2.17.2 → papers_cli-2.17.3}/tests/test_utils.py +0 -0
|
@@ -20,13 +20,17 @@ mitigations are built in:
|
|
|
20
20
|
- The package also installs the command under the unambiguous name
|
|
21
21
|
`papers-cli` — call that, or set a shell alias (e.g.
|
|
22
22
|
`alias papers=papers-cli`), on systems where the names collide.
|
|
23
|
-
- Because this command may mask GNOME Papers,
|
|
24
|
-
existing
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
30
34
|
|
|
31
35
|
## Dependencies
|
|
32
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__
|
|
@@ -794,7 +794,10 @@ def get_parser(config=None):
|
|
|
794
794
|
keyfmt = argparse.ArgumentParser(add_help=False)
|
|
795
795
|
grp = keyfmt.add_argument_group('bibtex key format')
|
|
796
796
|
grp.add_argument('--key-template', default=config.keyformat.template,
|
|
797
|
-
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.''')
|
|
798
801
|
grp.add_argument('--key-author-num', type=int, default=config.keyformat.author_num,
|
|
799
802
|
help='number of authors to include in key (default:%(default)s)')
|
|
800
803
|
grp.add_argument('--key-author-sep', default=config.keyformat.author_sep,
|
|
@@ -884,7 +887,10 @@ def get_parser(config=None):
|
|
|
884
887
|
grp.add_argument('--no-merge-files', action='store_true',
|
|
885
888
|
help='distinct "file" field considered a conflict, all other things being equal')
|
|
886
889
|
grp.add_argument('-u', '--update-key', action='store_true',
|
|
887
|
-
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)''')
|
|
888
894
|
# grp.add_argument('-f', '--force', action='store_true', help='no interactive')
|
|
889
895
|
grp.add_argument('-m', '--mode', default='i', choices=['u', 'U', 'o', 's', 'r', 'i', 'a'],
|
|
890
896
|
help='''if duplicates are found, the default is to start an (i)nteractive dialogue,
|
|
@@ -1119,6 +1125,10 @@ def handle_logging(o):
|
|
|
1119
1125
|
logger.debug("LOGGER LEVEL: "+logging.getLevelName(logger.getEffectiveLevel()))
|
|
1120
1126
|
|
|
1121
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
|
+
|
|
1122
1132
|
def main(args=None):
|
|
1123
1133
|
papers.config.DRYRUN = False # reset in case main() if called directly
|
|
1124
1134
|
if args is not None:
|
|
@@ -1138,16 +1148,31 @@ def main(args=None):
|
|
|
1138
1148
|
parser, subparsers = get_parser(config)
|
|
1139
1149
|
|
|
1140
1150
|
# viewer passthrough (#107): this command often masks the GNOME Papers
|
|
1141
|
-
# document viewer in $PATH
|
|
1142
|
-
#
|
|
1143
|
-
#
|
|
1144
|
-
# masked viewer
|
|
1145
|
-
#
|
|
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).
|
|
1146
1160
|
argv = sys.argv[1:]
|
|
1147
1161
|
if argv and all(not a.startswith('-') for a in argv) \
|
|
1148
1162
|
and argv[0] not in subparsers.choices \
|
|
1149
|
-
and all(os.path.isfile(a) for a in argv):
|
|
1150
|
-
|
|
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)
|
|
1151
1176
|
print(f"papers-cli: opening {f!r} with the system viewer "
|
|
1152
1177
|
f"(see `papers --help` for the bibliography tool)", file=sys.stderr)
|
|
1153
1178
|
view_pdf(f)
|
|
@@ -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
|
+
import os
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
from urllib.parse import quote
|
|
2
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,
|
|
@@ -258,31 +264,98 @@ class OpenCmdTest(LocalInstallTest):
|
|
|
258
264
|
|
|
259
265
|
def test_bare_file_argument_opens_viewer(self):
|
|
260
266
|
# `papers somefile.pdf` behaves like the (possibly masked) document
|
|
261
|
-
# viewer:
|
|
262
|
-
|
|
267
|
+
# viewer: without another `papers` in $PATH, the file is opened with
|
|
268
|
+
# the system viewer (issue #107)
|
|
263
269
|
f = self._path('direct.pdf')
|
|
264
270
|
open(f, 'w').write('x')
|
|
265
|
-
with patch(
|
|
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)
|
|
266
275
|
self.papers(f'{f}')
|
|
267
276
|
viewer.assert_called_once_with(f)
|
|
268
277
|
|
|
269
278
|
def test_subcommand_wins_over_file(self):
|
|
270
279
|
# a file named like a subcommand does not hijack the CLI
|
|
271
|
-
from unittest.mock import patch
|
|
272
280
|
open(self._path('status'), 'w').write('x')
|
|
273
|
-
with patch('papers.__main__.
|
|
281
|
+
with patch('papers.__main__.find_masked_viewer') as finder, \
|
|
282
|
+
patch('papers.__main__.view_pdf') as viewer:
|
|
274
283
|
self.papers('status')
|
|
284
|
+
finder.assert_not_called()
|
|
275
285
|
viewer.assert_not_called()
|
|
276
286
|
|
|
277
287
|
|
|
278
288
|
class ViewerPassthroughNoInstallTest(BaseTest):
|
|
279
289
|
anotherbib_content = None
|
|
280
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
|
+
|
|
281
300
|
def test_bare_file_argument_without_install(self):
|
|
282
301
|
# the viewer passthrough must work without any papers install
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|