papers-cli 2.4__py3-none-any.whl → 2.6__py3-none-any.whl
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/__main__.py +257 -109
- papers/_version.py +14 -2
- papers/bib.py +43 -25
- papers/config.py +47 -22
- papers/duplicate.py +1 -1
- papers/filename.py +42 -18
- papers/utils.py +57 -7
- {papers_cli-2.4.dist-info → papers_cli-2.6.dist-info}/METADATA +20 -11
- papers_cli-2.6.dist-info/RECORD +17 -0
- {papers_cli-2.4.dist-info → papers_cli-2.6.dist-info}/WHEEL +1 -1
- papers_cli-2.6.dist-info/entry_points.txt +2 -0
- papers_cli-2.4.dist-info/RECORD +0 -17
- papers_cli-2.4.dist-info/entry_points.txt +0 -2
- {papers_cli-2.4.dist-info → papers_cli-2.6.dist-info}/LICENSE +0 -0
- {papers_cli-2.4.dist-info → papers_cli-2.6.dist-info}/top_level.txt +0 -0
papers/__main__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""That is the script called by papers
|
|
1
|
+
"""That is the script called by the papers cli command
|
|
2
2
|
"""
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
@@ -15,22 +15,41 @@ from papers import logger
|
|
|
15
15
|
from papers.extract import extract_pdf_doi, isvaliddoi, extract_pdf_metadata
|
|
16
16
|
from papers.extract import fetch_bibtex_by_doi
|
|
17
17
|
from papers.encoding import parse_file, format_file, family_names, format_entries
|
|
18
|
-
from papers.config import bcolors, Config, search_config, CONFIG_FILE, DATA_DIR, CONFIG_FILE_LEGACY
|
|
18
|
+
from papers.config import bcolors, Config, search_config, CONFIG_FILE, CONFIG_FILE_LOCAL, DATA_DIR, CONFIG_FILE_LEGACY, BACKUP_DIR
|
|
19
19
|
from papers.duplicate import list_duplicates, list_uniques, edit_entries
|
|
20
|
-
from papers.bib import Biblio, FUZZY_RATIO, DEFAULT_SIMILARITY, entry_filecheck, backupfile, isvalidkey
|
|
20
|
+
from papers.bib import Biblio, FUZZY_RATIO, DEFAULT_SIMILARITY, entry_filecheck, backupfile as backupfile_func, isvalidkey
|
|
21
|
+
from papers.utils import move, checksum, ansi_link as link, view_pdf
|
|
21
22
|
from papers import __version__
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if not os.path.exists(
|
|
27
|
-
|
|
25
|
+
if os.path.exists(CONFIG_FILE_LEGACY):
|
|
26
|
+
# move config file from ~/.local/.share/papers/ to ~/.config/papersconfig.json .papers/config.json to .papersconfig.json"
|
|
27
|
+
if not os.path.exists(CONFIG_FILE):
|
|
28
|
+
logger.warning(f"Move legacy config file {CONFIG_FILE_LEGACY} to {CONFIG_FILE}'")
|
|
28
29
|
shutil.move(CONFIG_FILE_LEGACY, CONFIG_FILE)
|
|
29
|
-
|
|
30
|
+
else:
|
|
31
|
+
logger.warning(f"Legacy config file found: {CONFIG_FILE_LEGACY}. Delete to remove this warning: rm -f '{CONFIG_FILE_LEGACY}'")
|
|
32
|
+
|
|
33
|
+
def check_legacy_config(configfile):
|
|
34
|
+
" move config file from ~/.local/.share/papers/ to ~/.config/papersconfig.json and .papers/config.json to .papersconfig.json"
|
|
35
|
+
if os.path.exists(configfile):
|
|
36
|
+
p = Path(configfile)
|
|
37
|
+
if configfile == CONFIG_FILE_LEGACY:
|
|
38
|
+
shutil.move(configfile, CONFIG_FILE)
|
|
39
|
+
configfile = CONFIG_FILE
|
|
40
|
+
elif p.name == "config.json" and p.parent.name == ".papers":
|
|
41
|
+
newname = str(p.parent.parent/CONFIG_FILE_LOCAL)
|
|
42
|
+
shutil.move(configfile, newname)
|
|
43
|
+
configfile = newname
|
|
44
|
+
|
|
30
45
|
return configfile
|
|
31
46
|
|
|
32
47
|
|
|
33
48
|
def get_biblio(config):
|
|
49
|
+
"""
|
|
50
|
+
This function initializes a Biblio object based on the bibtex file specified as command line argument or in config file.
|
|
51
|
+
If no bibtex file is specified, it raises a ValueError().
|
|
52
|
+
"""
|
|
34
53
|
if config.bibtex is None:
|
|
35
54
|
raise ValueError('bibtex is not initialized')
|
|
36
55
|
relative_to = os.path.sep if config.absolute_paths else (os.path.dirname(config.bibtex) if config.bibtex else None)
|
|
@@ -44,91 +63,146 @@ def get_biblio(config):
|
|
|
44
63
|
|
|
45
64
|
|
|
46
65
|
def _backup_bib(biblio, config, message=None):
|
|
66
|
+
if not config.git:
|
|
67
|
+
raise PapersExit('cannot backup without --git enabled')
|
|
47
68
|
# backupdir = Path(config.file).parent
|
|
48
69
|
backupdir = Path(config.gitdir)
|
|
49
70
|
backupdir.mkdir(exist_ok=True)
|
|
50
|
-
backupfile = backupdir/'papers.bib'
|
|
51
71
|
|
|
52
72
|
# remove if exists
|
|
53
|
-
backupfile.unlink(missing_ok=True)
|
|
73
|
+
config.backupfile.unlink(missing_ok=True)
|
|
74
|
+
config.backupfile_clean.unlink(missing_ok=True)
|
|
54
75
|
|
|
55
76
|
## Here we could create a copy of biblio since it is modified in place
|
|
56
77
|
## For now, we exit the program after saving, so don't bother
|
|
78
|
+
logger.debug(f'BACKUP: cp {config.bibtex} {config.backupfile}')
|
|
79
|
+
shutil.copy(config.bibtex, config.backupfile)
|
|
80
|
+
config.gitcmd(f"add {config.backupfile.name}")
|
|
81
|
+
|
|
57
82
|
if config.backup_files:
|
|
58
83
|
logger.info('backup bibliography with files')
|
|
59
84
|
backupfilesdir = backupdir/"files"
|
|
60
85
|
backupfilesdir.mkdir(exist_ok=True)
|
|
61
86
|
biblio.filesdir = str(backupfilesdir)
|
|
62
|
-
biblio.rename_entries_files(copy=True, relative_to=backupdir)
|
|
63
|
-
biblio.save(
|
|
64
|
-
config.gitcmd(f"add
|
|
87
|
+
biblio.rename_entries_files(copy=True, relative_to=backupdir, hardlink=True)
|
|
88
|
+
biblio.save(config.backupfile_clean)
|
|
89
|
+
config.gitcmd(f"add {config.backupfile_clean.name}")
|
|
65
90
|
config.gitcmd(f"add files")
|
|
66
91
|
|
|
67
92
|
else:
|
|
68
93
|
logger.info('backup bibliography only (without files)')
|
|
69
94
|
biblio.update_file_path(relative_to=backupdir)
|
|
70
|
-
biblio.save(
|
|
71
|
-
config.gitcmd(f"add
|
|
95
|
+
biblio.save(config.backupfile_clean)
|
|
96
|
+
config.gitcmd(f"add {config.backupfile_clean.name}")
|
|
72
97
|
|
|
73
|
-
message = message or f'
|
|
98
|
+
message = message or f'papers ' +' '.join(sys.argv[1:])
|
|
74
99
|
res = config.gitcmd(f"commit -m '{message}'", check=False)
|
|
75
|
-
|
|
100
|
+
# work on "main" branch for comitting (out of history branch)
|
|
101
|
+
config.gitcmd(f"checkout -B main")
|
|
102
|
+
config.gitcmd(f"clean -f")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _restore_from_backupdir(config, restore_files=False):
|
|
106
|
+
restore_cmd = f"papers add {config.backupfile_clean} --rename --copy" if config.backup_files else f"cp {config.backupfile} {config.bibtex}"
|
|
107
|
+
repair_message = f"papers backup broken :: cannot repair file links :: try to recover manually with `{restore_cmd}`"
|
|
108
|
+
try:
|
|
109
|
+
return _restore_from_backupdir_wrapped(config, restore_files=restore_files)
|
|
110
|
+
except Exception as error:
|
|
111
|
+
raise
|
|
112
|
+
logger.error(str(error))
|
|
113
|
+
raise PapersExit(repair_message)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _restore_from_backupdir_wrapped(config, restore_files=False):
|
|
117
|
+
# current = sp.check_output(f"git rev-parse --short HEAD", shell=True, cwd=config.gitdir).strip().decode()
|
|
118
|
+
current = sp.check_output(f"git rev-parse HEAD", shell=True, cwd=config.gitdir).strip().decode()
|
|
119
|
+
message = sp.check_output(f"git log {current} --pretty='format:%C(auto)%h %s (%ad)' -1", shell=True, cwd=config.gitdir).strip().decode()
|
|
120
|
+
logger.info(f'restore bibliography to {message}')
|
|
76
121
|
|
|
77
|
-
def _restore_from_backupdir(config):
|
|
78
|
-
logger.info('restore bibliography')
|
|
79
|
-
# copy the files to the working directory
|
|
80
|
-
# backupdir = Path(config.file).parent
|
|
81
|
-
backupdir = Path(config.gitdir)
|
|
82
|
-
backupfile = backupdir/'papers.bib'
|
|
83
122
|
if os.path.exists(config.bibtex):
|
|
84
123
|
os.remove(config.bibtex)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
cmd = f"add {backupfile} --bibtex {config.bibtex} --filesdir {config.filesdir} --no-git"
|
|
90
|
-
# return sp.check_call(f"PYTHONPATH={Path(papers.__file__).parent.parent} python3 -m papers {cmd}", shell=True)
|
|
91
|
-
# Here we avoid starting a new process with re-importing python libs etc
|
|
92
|
-
logger.info("papers "+cmd)
|
|
93
|
-
main(cmd.split())
|
|
94
|
-
|
|
95
|
-
def _git_undo(config):
|
|
96
|
-
"""papers undo using git backup"""
|
|
97
|
-
# backupdir = Path(config.file).parent
|
|
98
|
-
backupdir = Path(config.gitdir)
|
|
99
|
-
backupfile = backupdir/'papers.bib'
|
|
100
|
-
# keep track of where we come from, for future redos
|
|
101
|
-
current = sp.check_output(f"git rev-parse HEAD", shell=True, cwd=backupdir).strip().decode()
|
|
102
|
-
# now go back
|
|
103
|
-
config.gitcmd(f"reset --hard HEAD^")
|
|
104
|
-
futuresfile = backupdir/"futures.txt"
|
|
105
|
-
open(futuresfile, 'a+').write(current+"\n")
|
|
106
|
-
_restore_from_backupdir(config)
|
|
107
|
-
|
|
108
|
-
def _git_redo(config):
|
|
109
|
-
"""papers redo using git backup"""
|
|
110
|
-
# backupdir = Path(config.file).parent
|
|
111
|
-
backupdir = Path(config.gitdir)
|
|
112
|
-
backupfile = backupdir/'papers.bib'
|
|
113
|
-
# keep track of where we come from, for future redos
|
|
114
|
-
futuresfile = backupdir/"futures.txt"
|
|
115
|
-
try:
|
|
116
|
-
futures = open(futuresfile, 'r').readlines()
|
|
117
|
-
last = futures[-1].strip()
|
|
118
|
-
except:
|
|
119
|
-
last = ""
|
|
120
|
-
if not last:
|
|
121
|
-
logger.info("Nothing to redo")
|
|
124
|
+
shutil.copy(config.backupfile, config.bibtex)
|
|
125
|
+
|
|
126
|
+
# Re-name the file according to back-up bibtex
|
|
127
|
+
if not config.backup_files:
|
|
122
128
|
return
|
|
123
129
|
|
|
124
|
-
config.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
biblio = Biblio.load(config.bibtex, config.filesdir)
|
|
131
|
+
biblio_clean = Biblio.load(config.backupfile_clean, config.filesdir)
|
|
132
|
+
|
|
133
|
+
assert len(biblio.entries) == len(biblio_clean.entries)
|
|
134
|
+
|
|
135
|
+
for e, e_clean in zip(biblio.entries, biblio_clean.entries):
|
|
136
|
+
assert e['ID'] == e_clean['ID'], f"{e['ID']} != {e_clean['ID']})"
|
|
137
|
+
files = biblio.get_files(e)
|
|
138
|
+
files_clean = biblio_clean.get_files(e_clean)
|
|
139
|
+
assert len(files) == len(files_clean), f"{e['ID']} :: files {len(files)} != {len(files_clean)})"
|
|
140
|
+
|
|
141
|
+
new_files = []
|
|
142
|
+
|
|
143
|
+
for f, f_clean in zip(files, files_clean):
|
|
144
|
+
# broken link in the backup
|
|
145
|
+
if not os.path.exists(f_clean):
|
|
146
|
+
logger.debug(f"BACKUP FILE DOES NOT EXISTS: {f_clean} ")
|
|
147
|
+
logger.debug(f"BACKUP ENTRY: {e_clean['file']} ")
|
|
148
|
+
logger.debug(f"BROKEN ENTRY: {e['file']} ")
|
|
149
|
+
logger.warning(f"{e['ID']} :: file link broken => {f} ")
|
|
150
|
+
new_files.append(f)
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# backup file is fine
|
|
154
|
+
|
|
155
|
+
# original bibtex link matches something on disk
|
|
156
|
+
if os.path.exists(f):
|
|
157
|
+
|
|
158
|
+
# same file: nothing to do
|
|
159
|
+
if os.path.samefile(f, f_clean) or checksum(f_clean) == checksum(f):
|
|
160
|
+
new_files.append(f)
|
|
161
|
+
|
|
162
|
+
else:
|
|
163
|
+
logger.warning(f"{e['ID']} :: file found but does not match backup (keep pointer to backup): {f} != {f_clean}")
|
|
164
|
+
new_files.append(f_clean)
|
|
165
|
+
|
|
166
|
+
# original bibtex link is broken, --restore-file is active
|
|
167
|
+
elif restore_files:
|
|
168
|
+
try:
|
|
169
|
+
move(f_clean, f, copy=True, interactive=False)
|
|
170
|
+
new_files.append(f)
|
|
171
|
+
|
|
172
|
+
except Exception as error:
|
|
173
|
+
logger.error(f"{error}")
|
|
174
|
+
logger.error(f"{e['ID']} :: failed to restore file (keep pointer to backup)")
|
|
175
|
+
new_files.append(f_clean)
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
# original bibtex link is broken, default without --restore-file : do not do anything
|
|
179
|
+
else:
|
|
180
|
+
new_files.append(f_clean)
|
|
181
|
+
|
|
182
|
+
biblio.set_files(e, new_files)
|
|
183
|
+
|
|
184
|
+
biblio.save(config.bibtex)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _git_reset_to_commit(config, commit, restore_files=False):
|
|
188
|
+
config.gitcmd(f"reset --hard {commit}")
|
|
189
|
+
_restore_from_backupdir(config, restore_files=restore_files)
|
|
130
190
|
|
|
131
|
-
|
|
191
|
+
|
|
192
|
+
def _git_undo(config, restore_files=False, steps=1):
|
|
193
|
+
sp.check_call(f"git checkout -B history", shell=True, cwd=config.gitdir)
|
|
194
|
+
_git_reset_to_commit(config, 'HEAD'+'^'*steps, restore_files=restore_files)
|
|
195
|
+
# _git_reset_to_commit(config, 'HEAD^', restore_files=o.restore_files)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _git_redo(config, restore_files=False, steps=1):
|
|
199
|
+
current = sp.check_output(f"git rev-parse HEAD", shell=True, cwd=config.gitdir).strip().decode()
|
|
200
|
+
futures = sp.check_output(f"git rev-list {current}..main", shell=True, cwd=config.gitdir).strip().decode().splitlines()[::-1]
|
|
201
|
+
try:
|
|
202
|
+
future = futures[steps-1]
|
|
203
|
+
except Exception as error:
|
|
204
|
+
raise PapersExit("nothing to redo")
|
|
205
|
+
_git_reset_to_commit(config, future, restore_files=restore_files)
|
|
132
206
|
|
|
133
207
|
|
|
134
208
|
def savebib(biblio, config):
|
|
@@ -143,6 +217,8 @@ def savebib(biblio, config):
|
|
|
143
217
|
biblio.save(config.bibtex)
|
|
144
218
|
if config.file and config.git:
|
|
145
219
|
_backup_bib(biblio, config)
|
|
220
|
+
else:
|
|
221
|
+
logger.debug(f'do not backup bib: {config.file}, {config.git}')
|
|
146
222
|
# if config.git:
|
|
147
223
|
# config.gitcommit()
|
|
148
224
|
|
|
@@ -150,7 +226,7 @@ def savebib(biblio, config):
|
|
|
150
226
|
def set_keyformat_config_from_cmd(o, config):
|
|
151
227
|
"""
|
|
152
228
|
Given options and a config state, applies the desired key options to the config.
|
|
153
|
-
"""
|
|
229
|
+
"""
|
|
154
230
|
config.keyformat.template = o.key_template
|
|
155
231
|
config.keyformat.author_num = o.key_author_num
|
|
156
232
|
config.keyformat.author_sep = o.key_author_sep
|
|
@@ -186,12 +262,18 @@ def installcmd(parser, o, config):
|
|
|
186
262
|
o.edit = ans.lower() == 'e'
|
|
187
263
|
|
|
188
264
|
if not o.edit:
|
|
265
|
+
if config.local and os.path.exists(config.file):
|
|
266
|
+
# if we don't do that the local install will take precedence over global install
|
|
267
|
+
logger.warning(f"remove pre-existing local configuration file: {config.file}")
|
|
268
|
+
os.remove(config.file)
|
|
189
269
|
config = Config()
|
|
190
270
|
|
|
191
271
|
if o.local is None:
|
|
192
272
|
if config.local is not None:
|
|
273
|
+
logger.debug(f'keep config to pre-existing: {config.local}')
|
|
193
274
|
o.local = config.local
|
|
194
275
|
else:
|
|
276
|
+
logger.debug(f'default to global config')
|
|
195
277
|
# default ?
|
|
196
278
|
o.local = False
|
|
197
279
|
|
|
@@ -203,11 +285,10 @@ def installcmd(parser, o, config):
|
|
|
203
285
|
default_filesdir = config.filesdir or "files"
|
|
204
286
|
|
|
205
287
|
if o.local:
|
|
206
|
-
papersconfig = config.file or
|
|
288
|
+
papersconfig = config.file or CONFIG_FILE_LOCAL
|
|
207
289
|
workdir = Path('.')
|
|
208
290
|
bibtex_files = [str(f) for f in sorted(workdir.glob("*.bib"))]
|
|
209
|
-
|
|
210
|
-
|
|
291
|
+
|
|
211
292
|
if o.absolute_paths is None:
|
|
212
293
|
o.absolute_paths = False
|
|
213
294
|
|
|
@@ -216,7 +297,7 @@ def installcmd(parser, o, config):
|
|
|
216
297
|
workdir = Path(DATA_DIR)
|
|
217
298
|
bibtex_files = [str(f) for f in sorted(Path('.').glob("*.bib"))] + [str(f) for f in sorted(workdir.glob("*.bib"))]
|
|
218
299
|
checkdirs = [os.path.join(DATA_DIR, "files")] + checkdirs
|
|
219
|
-
|
|
300
|
+
|
|
220
301
|
if o.absolute_paths is None:
|
|
221
302
|
o.absolute_paths = True
|
|
222
303
|
|
|
@@ -271,16 +352,25 @@ def installcmd(parser, o, config):
|
|
|
271
352
|
config.bibtex = o.bibtex
|
|
272
353
|
config.filesdir = o.filesdir
|
|
273
354
|
config.file = papersconfig
|
|
274
|
-
config.gitdir = config.data = os.path.dirname(config.file)
|
|
275
355
|
config.local = o.local
|
|
276
356
|
config.absolute_paths = o.absolute_paths
|
|
277
357
|
|
|
358
|
+
# Unless otherwise specified (option not advertized -- help suppressed)
|
|
359
|
+
# the git directory is centralized in the back up dir.
|
|
360
|
+
if o.gitdir:
|
|
361
|
+
config.gitdir = o.gitdir
|
|
362
|
+
|
|
363
|
+
elif config.bibtex is not None:
|
|
364
|
+
from normality import slugify
|
|
365
|
+
bibi = config.bibtex[:-len(".bib")] if config.bibtex.endswith(".bib") else config.bibtex
|
|
366
|
+
config.gitdir = os.path.join(BACKUP_DIR, slugify(bibi))
|
|
367
|
+
|
|
278
368
|
if o.editor:
|
|
279
369
|
config.editor = o.editor
|
|
280
370
|
|
|
281
371
|
# create bibtex file if not existing
|
|
282
372
|
bibtex = Path(o.bibtex) if o.bibtex else None
|
|
283
|
-
|
|
373
|
+
|
|
284
374
|
if bibtex and not bibtex.exists():
|
|
285
375
|
logger.info(f'create empty bibliography database: {bibtex}')
|
|
286
376
|
bibtex.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -325,32 +415,27 @@ def installcmd(parser, o, config):
|
|
|
325
415
|
config.backup_files = config.gitlfs
|
|
326
416
|
|
|
327
417
|
logger.info('save config file: '+config.file)
|
|
328
|
-
os.
|
|
418
|
+
if os.path.dirname(config.file):
|
|
419
|
+
# typically is current dir = ""
|
|
420
|
+
os.makedirs(os.path.dirname(config.file), exist_ok=True)
|
|
329
421
|
|
|
330
422
|
config.git = o.git
|
|
331
423
|
|
|
332
424
|
config.save()
|
|
333
425
|
|
|
334
426
|
if config.git:
|
|
427
|
+
# add a gitignore file to skip gitdir
|
|
335
428
|
if (Path(config.gitdir)/'.git').exists():
|
|
336
429
|
logger.warning(f'{config.gitdir} is already initialized')
|
|
337
430
|
else:
|
|
338
431
|
os.makedirs(config.gitdir, exist_ok=True)
|
|
339
432
|
config.gitcmd('init')
|
|
340
433
|
|
|
341
|
-
|
|
342
434
|
if config.gitlfs:
|
|
343
435
|
config.gitcmd('lfs track "files/"')
|
|
344
436
|
config.gitcmd('add .gitattributes')
|
|
437
|
+
config.gitcmd(f'commit -m "papers install: .gitattribute"', check=False)
|
|
345
438
|
|
|
346
|
-
with open(Path(config.gitdir)/'.gitignore', 'a+') as f:
|
|
347
|
-
lines = f.readlines()
|
|
348
|
-
if 'futures.txt' not in (l.strip() for l in lines):
|
|
349
|
-
f.write('futures.txt\n')
|
|
350
|
-
config.gitcmd('add .gitignore')
|
|
351
|
-
config.gitcmd(f'add {os.path.abspath(config.file)}')
|
|
352
|
-
message = f'papers ' +' '.join(sys.argv[1:])
|
|
353
|
-
config.gitcmd(f'commit -m "new install: config file"', check=False)
|
|
354
439
|
biblio = get_biblio(config)
|
|
355
440
|
_backup_bib(biblio, config)
|
|
356
441
|
|
|
@@ -362,7 +447,7 @@ def _dir_is_empty(dir):
|
|
|
362
447
|
return not any(it)
|
|
363
448
|
|
|
364
449
|
def uninstallcmd(parser, o, config):
|
|
365
|
-
# TODO this is actually never tested.
|
|
450
|
+
# TODO this is actually never tested.
|
|
366
451
|
if Path(config.file).exists():
|
|
367
452
|
logger.info(f"The uninstaller will now remove {config.file}")
|
|
368
453
|
os.remove(config.file)
|
|
@@ -376,11 +461,11 @@ def uninstallcmd(parser, o, config):
|
|
|
376
461
|
return
|
|
377
462
|
|
|
378
463
|
if o.recursive:
|
|
379
|
-
config.file = search_config([os.path.join(".papers", "config.json")], start_dir=".", default=CONFIG_FILE)
|
|
464
|
+
config.file = search_config([CONFIG_FILE_LOCAL, os.path.join(".papers", "config.json")], start_dir=".", default=CONFIG_FILE)
|
|
380
465
|
config.file = check_legacy_config(config.file)
|
|
381
466
|
uninstallcmd(parser, o, config)
|
|
382
467
|
|
|
383
|
-
def check_install(parser, o, config):
|
|
468
|
+
def check_install(parser, o, config, bibtex_must_exist=True):
|
|
384
469
|
"""
|
|
385
470
|
Given an option and config, checks to see if the install is done correctly on this filesystem.
|
|
386
471
|
"""
|
|
@@ -398,7 +483,8 @@ def check_install(parser, o, config):
|
|
|
398
483
|
parser.print_help()
|
|
399
484
|
print(f"--bibtex must be specified, or {install_doc}")
|
|
400
485
|
raise PapersExit()
|
|
401
|
-
|
|
486
|
+
|
|
487
|
+
if bibtex_must_exist and not os.path.exists(config.bibtex):
|
|
402
488
|
print(f'papers: error: no bibtex file found, do `touch {config.bibtex}` or {install_doc}')
|
|
403
489
|
raise PapersExit()
|
|
404
490
|
logger.info(f'bibtex: {config.bibtex!r}')
|
|
@@ -408,7 +494,7 @@ def check_install(parser, o, config):
|
|
|
408
494
|
|
|
409
495
|
def addcmd(parser, o, config):
|
|
410
496
|
"""
|
|
411
|
-
Given an options set and a config, sets up the function call to add the file or
|
|
497
|
+
Given an options set and a config, sets up the function call to add the file or all files in the directory to the bibtex, and executes it.
|
|
412
498
|
"""
|
|
413
499
|
|
|
414
500
|
set_nameformat_config_from_cmd(o, config)
|
|
@@ -447,7 +533,7 @@ def addcmd(parser, o, config):
|
|
|
447
533
|
**kw)
|
|
448
534
|
else:
|
|
449
535
|
raise ValueError(file+' is a directory, requires --recursive to explore')
|
|
450
|
-
|
|
536
|
+
|
|
451
537
|
elif file.endswith('.pdf'):
|
|
452
538
|
biblio.add_pdf(file, attachments=o.attachment, rename=o.rename, copy=o.copy,
|
|
453
539
|
search_doi=not o.no_query_doi,
|
|
@@ -456,7 +542,7 @@ def addcmd(parser, o, config):
|
|
|
456
542
|
**kw)
|
|
457
543
|
|
|
458
544
|
else: # file.endswith('.bib'):
|
|
459
|
-
biblio.add_bibtex_file(file, **kw)
|
|
545
|
+
biblio.add_bibtex_file(file, attachments=o.attachment, rename=o.rename, copy=o.copy, **kw)
|
|
460
546
|
|
|
461
547
|
except Exception as error:
|
|
462
548
|
# print(error)
|
|
@@ -471,10 +557,13 @@ def addcmd(parser, o, config):
|
|
|
471
557
|
savebib(biblio, config)
|
|
472
558
|
|
|
473
559
|
def checkcmd(parser, o, config):
|
|
560
|
+
"""
|
|
561
|
+
Loops over the entire bib file that the Papers install sees, and checks each entry for formatting and for the existance of duplicates. Then writes the Biblio object back to your Bibtex file.
|
|
562
|
+
"""
|
|
474
563
|
set_keyformat_config_from_cmd(o, config)
|
|
475
564
|
|
|
476
565
|
biblio = get_biblio(config)
|
|
477
|
-
|
|
566
|
+
|
|
478
567
|
# if o.fix_all:
|
|
479
568
|
# o.fix_doi = True
|
|
480
569
|
# o.fetch_all = True
|
|
@@ -505,21 +594,21 @@ def filecheckcmd(parser, o, config):
|
|
|
505
594
|
|
|
506
595
|
if o.rename:
|
|
507
596
|
biblio.rename_entries_files(o.copy)
|
|
508
|
-
|
|
597
|
+
|
|
509
598
|
savebib(biblio, config)
|
|
510
599
|
|
|
511
600
|
def redocmd(parser, o, config):
|
|
512
601
|
if config.git:
|
|
513
|
-
return _git_redo(config)
|
|
602
|
+
return _git_redo(config, restore_files=o.restore_files, steps=o.steps)
|
|
514
603
|
else:
|
|
515
604
|
undocmd(parser, o, config)
|
|
516
605
|
|
|
517
606
|
def undocmd(parser, o, config):
|
|
518
607
|
if config.git:
|
|
519
|
-
return _git_undo(config)
|
|
608
|
+
return _git_undo(config, restore_files=o.restore_files, steps=o.steps)
|
|
520
609
|
|
|
521
610
|
logger.warning("git-tracking is not installed: undo / redo is limited to 1 step back and forth")
|
|
522
|
-
back =
|
|
611
|
+
back = backupfile_func(config.bibtex)
|
|
523
612
|
tmp = config.bibtex + '.tmp'
|
|
524
613
|
# my = :config.bibtex, config.filesdir)
|
|
525
614
|
logger.info(config.bibtex+' <==> '+back)
|
|
@@ -528,6 +617,16 @@ def undocmd(parser, o, config):
|
|
|
528
617
|
shutil.move(tmp, back)
|
|
529
618
|
# o.savebib()
|
|
530
619
|
|
|
620
|
+
def restorecmd(parser, o, config):
|
|
621
|
+
if not config.git:
|
|
622
|
+
parser.print_help()
|
|
623
|
+
raise PapersExit('only valid with --git enabled')
|
|
624
|
+
if o.ref:
|
|
625
|
+
_git_reset_to_commit(config, o.ref, restore_files=o.restore_files)
|
|
626
|
+
else:
|
|
627
|
+
_restore_from_backupdir(config, restore_files=o.restore_files)
|
|
628
|
+
|
|
629
|
+
|
|
531
630
|
def gitcmd(parser, o, config):
|
|
532
631
|
try:
|
|
533
632
|
out = sp.check_output(['git']+o.gitargs, cwd=config.gitdir)
|
|
@@ -537,7 +636,7 @@ def gitcmd(parser, o, config):
|
|
|
537
636
|
parser.error('papers failed to execute git command -- you should check your system git install.')
|
|
538
637
|
|
|
539
638
|
def doicmd(parser, o):
|
|
540
|
-
print(extract_pdf_doi(o.pdf, image=o.image))
|
|
639
|
+
print(extract_pdf_doi(o.pdf, image=o.image))
|
|
541
640
|
|
|
542
641
|
def fetchcmd(parser, o):
|
|
543
642
|
print(fetch_bibtex_by_doi(o.doi))
|
|
@@ -665,6 +764,20 @@ def listcmd(parser, o, config):
|
|
|
665
764
|
e['keywords'] = ", ".join(keywords)
|
|
666
765
|
savebib(biblio, config)
|
|
667
766
|
|
|
767
|
+
elif o.add_files:
|
|
768
|
+
if len(entries) != 1:
|
|
769
|
+
raise PapersExit("list only one entry to use --add-files")
|
|
770
|
+
e = entries[0]
|
|
771
|
+
files = biblio.get_files(e)
|
|
772
|
+
for f in o.add_files:
|
|
773
|
+
if not os.path.exists(f):
|
|
774
|
+
raise PapersExit(f"file {f} does not exist")
|
|
775
|
+
files.extend(o.add_files)
|
|
776
|
+
biblio.set_files(e, files)
|
|
777
|
+
if o.rename:
|
|
778
|
+
biblio.rename_entry_files(e, copy=o.copy)
|
|
779
|
+
savebib(biblio, config)
|
|
780
|
+
|
|
668
781
|
elif o.edit:
|
|
669
782
|
otherentries = [e for e in biblio.db.entries if e not in entries]
|
|
670
783
|
try:
|
|
@@ -675,7 +788,7 @@ def listcmd(parser, o, config):
|
|
|
675
788
|
return
|
|
676
789
|
|
|
677
790
|
savebib(biblio, config)
|
|
678
|
-
|
|
791
|
+
|
|
679
792
|
elif o.fetch:
|
|
680
793
|
for e in entries:
|
|
681
794
|
biblio.fix_entry(e, fix_doi=True, fix_key=True, fetch_all=True, interactive=True)
|
|
@@ -686,6 +799,12 @@ def listcmd(parser, o, config):
|
|
|
686
799
|
biblio.db.entries.remove(e)
|
|
687
800
|
savebib(biblio, config)
|
|
688
801
|
|
|
802
|
+
elif o.open:
|
|
803
|
+
for e in entries:
|
|
804
|
+
for f in biblio.get_files(e):
|
|
805
|
+
logger.info(f"opening {f} ...")
|
|
806
|
+
view_pdf(f)
|
|
807
|
+
|
|
689
808
|
elif o.field:
|
|
690
809
|
# entries = [{k:e[k] for k in e if k in o.field+['ID','ENTRYTYPE']} for e in entries]
|
|
691
810
|
for e in entries:
|
|
@@ -698,21 +817,26 @@ def listcmd(parser, o, config):
|
|
|
698
817
|
tit = e.get('title', '')[:60]+ ('...' if len(e.get('title', ''))>60 else '')
|
|
699
818
|
info = []
|
|
700
819
|
if e.get('doi',''):
|
|
701
|
-
info.append('doi:'+e['doi'])
|
|
820
|
+
info.append(link(f"https://doi.org/{e['doi']}", 'doi:'+e['doi']))
|
|
702
821
|
n = _nfiles(e)
|
|
703
822
|
if n:
|
|
704
|
-
|
|
823
|
+
files = parse_file(e.get('file',''), relative_to=biblio.relative_to)
|
|
824
|
+
file_link = f"file:///{Path(files[0]).resolve()}"
|
|
825
|
+
ansi_link = link(file_link, f'{"file" if n == 1 else "files"}:{str(n)}')
|
|
826
|
+
info.append(bcolors.OKGREEN+ansi_link+bcolors.ENDC)
|
|
705
827
|
if e.get('keywords',''):
|
|
706
828
|
keywords = parse_keywords(e)
|
|
707
829
|
info.append(bcolors.WARNING+" | ".join(keywords)+bcolors.ENDC)
|
|
708
830
|
infotag = '('+', '.join(info)+')' if info else ''
|
|
709
831
|
print(key(e), tit, infotag)
|
|
832
|
+
|
|
710
833
|
else:
|
|
711
834
|
print(format_entries(entries))
|
|
712
835
|
|
|
836
|
+
|
|
713
837
|
def statuscmd(parser, o, config):
|
|
714
838
|
print(config.status(check_files=not o.no_check_files, verbose=o.verbose))
|
|
715
|
-
|
|
839
|
+
|
|
716
840
|
|
|
717
841
|
|
|
718
842
|
def get_parser(config=None):
|
|
@@ -930,7 +1054,7 @@ def get_parser(config=None):
|
|
|
930
1054
|
|
|
931
1055
|
# list
|
|
932
1056
|
# ======
|
|
933
|
-
listp = subparsers.add_parser('list', description='list (a subset of) entries',
|
|
1057
|
+
listp = subparsers.add_parser('list', description='list (a subset of) entries in the existing bib file',
|
|
934
1058
|
parents=[cfg])
|
|
935
1059
|
|
|
936
1060
|
listp.add_argument('fullsearch', nargs='*', help='''Search field. Usually no quotes required. See keywords to search specific fields. All words must find a match, unless --any is passed.''')
|
|
@@ -976,6 +1100,10 @@ def get_parser(config=None):
|
|
|
976
1100
|
grp.add_argument('--edit', action='store_true', help='interactive edit text file with entries, and re-insert them')
|
|
977
1101
|
grp.add_argument('--fetch', action='store_true', help='fetch and fix metadata')
|
|
978
1102
|
grp.add_argument('--add-keywords', '--add-tag', nargs='+', help='add keywords to the selected entries')
|
|
1103
|
+
grp.add_argument('--add-files', nargs='+', help='add files to the selected entries (only one entry must be listed)')
|
|
1104
|
+
grp.add_argument('--rename', action='store_true', help='rename added file(s) into files folder. Used together with --add-files')
|
|
1105
|
+
grp.add_argument('--copy', action='store_true', help='copy added file(s) into files folder. Used together with --add-files --rename')
|
|
1106
|
+
grp.add_argument('-o', '--open', action='store_true', help='open attachments (if any)')
|
|
979
1107
|
|
|
980
1108
|
# grp.add_argument('--merge-duplicates', action='store_true')
|
|
981
1109
|
|
|
@@ -1005,8 +1133,16 @@ def get_parser(config=None):
|
|
|
1005
1133
|
|
|
1006
1134
|
# undo
|
|
1007
1135
|
# ====
|
|
1008
|
-
|
|
1009
|
-
|
|
1136
|
+
_restorep = argparse.ArgumentParser(add_help=False)
|
|
1137
|
+
_restorep.add_argument('--restore-files', action='store_true', help='Use this option to restore files that have been renamed. By default the file link points to the back-up repository. This command has no effect without --git-lfs, and will result in broken file links.')
|
|
1138
|
+
|
|
1139
|
+
_stepsp = argparse.ArgumentParser(add_help=False)
|
|
1140
|
+
_stepsp.add_argument('-n', '--steps', type=int, default=1, help='number of times undo/redo should be performed')
|
|
1141
|
+
|
|
1142
|
+
undop = subparsers.add_parser('undo', parents=[cfg, _restorep, _stepsp], help='Undo changes on bibtex (if --git is not enabled, only back and forth with last modification). If --git-lfs is enabled, the file entry may differ if it does not exist on disk any more, unless --restore-files was passed.')
|
|
1143
|
+
redop = subparsers.add_parser('redo', parents=[cfg, _restorep, _stepsp], help='Redo changes on bibtex (if --git is not enabled, this has the same effect as papers undo)')
|
|
1144
|
+
restorep = subparsers.add_parser('restore-backup', parents=[cfg, _restorep], help='Restore bibtex from backup. Also restore files if --restore-files if passed (--git-lfs only).')
|
|
1145
|
+
restorep.add_argument('--ref', help='Optional: restore specific commit (execute `papers git whatchanged` to obtain appropriate reference)')
|
|
1010
1146
|
|
|
1011
1147
|
# git
|
|
1012
1148
|
# ===
|
|
@@ -1021,8 +1157,11 @@ def get_parser(config=None):
|
|
|
1021
1157
|
|
|
1022
1158
|
def main(args=None):
|
|
1023
1159
|
papers.config.DRYRUN = False # reset in case main() if called directly
|
|
1160
|
+
if args is not None:
|
|
1161
|
+
# used in the commit message
|
|
1162
|
+
sys.argv = sys.argv[:1] + args
|
|
1024
1163
|
|
|
1025
|
-
configfile = search_config([os.path.join(".papers", "config.json")], start_dir=".", default=CONFIG_FILE)
|
|
1164
|
+
configfile = search_config([CONFIG_FILE_LOCAL, os.path.join(".papers", "config.json")], start_dir=".", default=CONFIG_FILE)
|
|
1026
1165
|
configfile = check_legacy_config(configfile)
|
|
1027
1166
|
if not os.path.exists(configfile):
|
|
1028
1167
|
config = Config()
|
|
@@ -1079,6 +1218,8 @@ def main(args=None):
|
|
|
1079
1218
|
check_install(subp, o, config) and undocmd(subp, o, config)
|
|
1080
1219
|
elif o.cmd == 'redo':
|
|
1081
1220
|
check_install(subp, o, config) and redocmd(subp, o, config)
|
|
1221
|
+
elif o.cmd == 'restore-backup':
|
|
1222
|
+
check_install(subp, o, config, bibtex_must_exist=False) and restorecmd(subp, o, config)
|
|
1082
1223
|
elif o.cmd == 'git':
|
|
1083
1224
|
if not installed:
|
|
1084
1225
|
subp.error('papers must be installed to use git command')
|
|
@@ -1099,10 +1240,17 @@ class PapersExit(Exception):
|
|
|
1099
1240
|
pass
|
|
1100
1241
|
|
|
1101
1242
|
|
|
1243
|
+
def main_clean_exit(args=None):
|
|
1244
|
+
try:
|
|
1245
|
+
main(args)
|
|
1246
|
+
except PapersExit as error:
|
|
1247
|
+
if logger.getEffectiveLevel() == logging.DEBUG:
|
|
1248
|
+
raise
|
|
1249
|
+
if error.args:
|
|
1250
|
+
logger.error(str(error))
|
|
1251
|
+
sys.exit(1)
|
|
1252
|
+
|
|
1102
1253
|
if __name__ == "__main__":
|
|
1103
1254
|
# we use try/except here to use a clean exit instead of trace
|
|
1104
1255
|
# test and debugging may use main() directly for speed-up => better to avoid sys.exit there
|
|
1105
|
-
|
|
1106
|
-
main()
|
|
1107
|
-
except PapersExit:
|
|
1108
|
-
sys.exit(1)
|
|
1256
|
+
main_clean_exit()
|
papers/_version.py
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
# file generated by setuptools_scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '2.6'
|
|
16
|
+
__version_tuple__ = version_tuple = (2, 6)
|