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.
Files changed (78) hide show
  1. {papers_cli-2.17.1 → papers_cli-2.17.3}/PKG-INFO +1 -1
  2. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/installation.md +17 -4
  3. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/__main__.py +63 -8
  4. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/_version.py +3 -3
  5. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/utils.py +36 -0
  6. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/PKG-INFO +1 -1
  7. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/SOURCES.txt +2 -0
  8. papers_cli-2.17.3/papers_cli.egg-info/scm_file_list.json +71 -0
  9. papers_cli-2.17.3/papers_cli.egg-info/scm_version.json +8 -0
  10. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_add.py +16 -0
  11. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_list.py +114 -1
  12. {papers_cli-2.17.1 → papers_cli-2.17.3}/.gitattributes +0 -0
  13. {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/FUNDING.yml +0 -0
  14. {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/workflows/docs.yml +0 -0
  15. {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/workflows/pypi.yml +0 -0
  16. {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/workflows/release.yml +0 -0
  17. {papers_cli-2.17.1 → papers_cli-2.17.3}/.github/workflows/tests.yml +0 -0
  18. {papers_cli-2.17.1 → papers_cli-2.17.3}/.gitignore +0 -0
  19. {papers_cli-2.17.1 → papers_cli-2.17.3}/LICENSE +0 -0
  20. {papers_cli-2.17.1 → papers_cli-2.17.3}/README.md +0 -0
  21. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/adding-entries.md +0 -0
  22. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/comparison.md +0 -0
  23. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/configuration.md +0 -0
  24. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/features.md +0 -0
  25. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/git.md +0 -0
  26. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/index.md +0 -0
  27. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/library-management.md +0 -0
  28. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/listing.md +0 -0
  29. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/quickstart.md +0 -0
  30. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/renaming.md +0 -0
  31. {papers_cli-2.17.1 → papers_cli-2.17.3}/docs/undo-redo.md +0 -0
  32. {papers_cli-2.17.1 → papers_cli-2.17.3}/mkdocs.yml +0 -0
  33. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/__init__.py +0 -0
  34. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/backup.py +0 -0
  35. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/bib.py +0 -0
  36. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/config.py +0 -0
  37. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/duplicate.py +0 -0
  38. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/encoding.py +0 -0
  39. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/entries.py +0 -0
  40. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/extract.py +0 -0
  41. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/filename.py +0 -0
  42. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/install.py +0 -0
  43. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers/latexenc.py +0 -0
  44. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/dependency_links.txt +0 -0
  45. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/entry_points.txt +0 -0
  46. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/requires.txt +0 -0
  47. {papers_cli-2.17.1 → papers_cli-2.17.3}/papers_cli.egg-info/top_level.txt +0 -0
  48. {papers_cli-2.17.1 → papers_cli-2.17.3}/pyproject.toml +0 -0
  49. {papers_cli-2.17.1 → papers_cli-2.17.3}/requirements.txt +0 -0
  50. {papers_cli-2.17.1 → papers_cli-2.17.3}/scripts/README.md +0 -0
  51. {papers_cli-2.17.1 → papers_cli-2.17.3}/scripts/benchmark_bibtexparser_load.py +0 -0
  52. {papers_cli-2.17.1 → papers_cli-2.17.3}/scripts/generate_dummy_bib.py +0 -0
  53. {papers_cli-2.17.1 → papers_cli-2.17.3}/setup.cfg +0 -0
  54. {papers_cli-2.17.1 → papers_cli-2.17.3}/setup.py +0 -0
  55. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/__init__.py +0 -0
  56. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/common.py +0 -0
  57. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/config.py +0 -0
  58. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/download.py +0 -0
  59. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/.gitignore +0 -0
  60. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/bg-8-515-2011.pdf +0 -0
  61. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/bg-8-515-2011.txt +0 -0
  62. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/esd-4-11-2013-supplement.pdf +0 -0
  63. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/downloadedpapers/esd-4-11-2013.pdf +0 -0
  64. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_backup_dirs.py +0 -0
  65. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_bib_unit.py +0 -0
  66. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_biblio.py +0 -0
  67. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_check.py +0 -0
  68. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_config_unit.py +0 -0
  69. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_duplicates.py +0 -0
  70. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_encoding.py +0 -0
  71. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_extract.py +0 -0
  72. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_extract_unit.py +0 -0
  73. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_filecheck.py +0 -0
  74. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_filename_unit.py +0 -0
  75. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_install.py +0 -0
  76. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_latexenc.py +0 -0
  77. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_undo.py +0 -0
  78. {papers_cli-2.17.1 → papers_cli-2.17.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papers-cli
3
- Version: 2.17.1
3
+ Version: 2.17.3
4
4
  Summary: utilities to keep your PDF library organized
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT
@@ -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. The
18
- package therefore also installs the command under the unambiguous name
19
- `papers-cli` — call that, or set a shell alias (e.g.
20
- `alias papers=papers-cli`), on systems where the names collide.
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 = get_biblio(config)
735
- entries_by_key = {biblio.key(e): e for e in biblio.entries}
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='update added key according to any existing duplicate (otherwise an error might be raised on identical insert key)')
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 the file(s) attached to entries', parents=[cfg])
1044
- openp.add_argument('key', nargs='+', help='entry key(s), case-insensitive (shortcut for `papers list --key KEY --open`)')
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
- check_install(subp, o, config) and opencmd(subp, o, config)
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.1'
22
- __version_tuple__ = version_tuple = (2, 17, 1)
21
+ __version__ = version = '2.17.3'
22
+ __version_tuple__ = version_tuple = (2, 17, 3)
23
23
 
24
- __commit_id__ = commit_id = 'g6a373feeb'
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papers-cli
3
- Version: 2.17.1
3
+ Version: 2.17.3
4
4
  Summary: utilities to keep your PDF library organized
5
5
  Author-email: Mahé Perrette <mahe.perrette@gmail.com>
6
6
  License: MIT
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.17.3",
3
+ "distance": 0,
4
+ "node": "g7999cf08232dd30a1acd83196023e069aaa3dd48",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-01"
8
+ }
@@ -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
- from tests.common import LocalInstallTest, Biblio, tempfile
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