Ticket #3150: cmd-1.py

File cmd-1.py, 18.9 KB (added by Eric Pettersen, 6 years ago)

Added by email2trac

Line 
1# vim: set expandtab shiftwidth=4 softtabstop=4:
2
3# === UCSF ChimeraX Copyright ===
4# Copyright 2016 Regents of the University of California.
5# All rights reserved. This software provided pursuant to a
6# license agreement containing restrictions on its disclosure,
7# duplication and use. For details see:
8# http://www.rbvi.ucsf.edu/chimerax/docs/licensing.html
9# This notice must be embedded in or attached to all copies,
10# including partial copies, of the software or any revisions
11# or derivations thereof.
12# === UCSF ChimeraX Copyright ===
13
14from chimerax.core.commands import CmdDesc, register, Command, OpenFileNamesArg, RestOfLine, next_token, \
15 FileNameArg, BoolArg, StringArg, DynamicEnum
16from chimerax.core.commands.cli import RegisteredCommandInfo
17from chimerax.core.errors import UserError, LimitationError
18
19# need to use non-repeatable OpenFilesNamesArg (rather than OpenFileNameArg) so that 'browse' can still be
20# used to open multiple files
21class OpenFileNamesArgNoRepeat(OpenFileNamesArg):
22 allow_repeat = False
23
24import os.path
25def likely_pdb_id(text):
26 return not os.path.exists(text) and len(text) == 4 and text[0].isdigit() and text[1:].isalnum()
27
28def cmd_open(session, file_names, rest_of_line, *, log=True):
29 tokens = []
30 remainder = rest_of_line
31 while remainder:
32 token, token_log, remainder = next_token(remainder)
33 remainder = remainder.lstrip()
34 tokens.append(token)
35 database_name = format_name = None
36 for i in range(len(tokens)-2, -1, -2):
37 test_token = tokens[i].lower()
38 if "format".startswith(test_token):
39 format_name = tokens[i+1]
40 elif "fromdatabase".startswith(test_token):
41 database_name = tokens[i+1]
42
43 from .manager import NoOpenerError
44 mgr = session.open_command
45 fetches, files = fetches_vs_files(mgr, file_names, format_name, database_name)
46 if fetches:
47 try:
48 provider_args = mgr.fetch_args(fetches[0][1], format_name=fetches[0][2])
49 except NoOpenerError as e:
50 raise LimitationError(str(e))
51 else:
52 data_format = file_format(session, files[0], format_name)
53 if data_format is None:
54 # let provider_open raise the error, which will show the command
55 provider_args = {}
56 else:
57 try:
58 provider_args = mgr.open_args(data_format)
59 except NoOpenerError as e:
60 raise LimitationError(str(e))
61
62 provider_cmd_text = "open " + " ".join([FileNameArg.unparse(fn)
63 for fn in file_names] + tokens)
64 # register a private 'open' command that handles the provider's keywords
65 registry = RegisteredCommandInfo()
66 def format_names(ses=session):
67 fmt_names = set([ fmt.nicknames[0] for fmt in ses.open_command.open_data_formats ])
68 for db_name in ses.open_command.database_names:
69 for fmt_name in ses.open_command.database_info(db_name).keys():
70 fmt_names.add(ses.data_formats[fmt_name].nicknames[0])
71 return fmt_names
72
73 def database_names(mgr=mgr):
74 return mgr.database_names
75
76 keywords = {
77 'format': DynamicEnum(format_names),
78 'from_database': DynamicEnum(database_names),
79 'ignore_cache': BoolArg,
80 'name': StringArg
81 }
82 for keyword, annotation in provider_args.items():
83 if keyword in keywords:
84 raise ValueError("Open-provider keyword '%s' conflicts with builtin arg of"
85 " same name" % keyword)
86 keywords[keyword] = annotation
87 desc = CmdDesc(required=[('names', OpenFileNamesArg)], keyword=keywords.items(),
88 synopsis="unnecessary")
89 register("open", desc, provider_open, registry=registry)
90 Command(session, registry=registry).run(provider_cmd_text, log=log)
91
92def provider_open(session, names, format=None, from_database=None, ignore_cache=False,
93 name=None, _return_status=False, _add_models=True, **provider_kw):
94 mgr = session.open_command
95 # since the "file names" may be globs, need to preprocess them...
96 fetches, file_names = fetches_vs_files(mgr, names, format, from_database)
97 file_infos = [FileInfo(session, fn, format) for fn in file_names]
98 formats = set([fi.data_format for fi in file_infos])
99 databases = set([f[1:] for f in fetches])
100 homogeneous = len(formats) + len(databases) == 1
101 if provider_kw and not homogeneous:
102 raise UserError("Cannot provide format/database-specific keywords when opening"
103 " multiple different formats or databases; use several 'open' commands"
104 " instead.")
105 opened_models = []
106 statuses = []
107 if homogeneous:
108 data_format = formats.pop() if formats else None
109 database_name, format = databases.pop() if databases else (None, format)
110 if database_name:
111 fetcher_info, default_format_name = _fetch_info(mgr, database_name, format)
112 for ident, database_name, format_name in fetches:
113 if format_name is None:
114 format_name = default_format_name
115 models, status = collated_open(session, database_name, ident,
116 session.data_formats[format_name], _add_models, fetcher_info.fetch,
117 (session, ident, format_name, ignore_cache), provider_kw)
118 if status:
119 statuses.append(status)
120 if models:
121 opened_models.append(name_and_group_models(models, name, [ident]))
122 else:
123 opener_info, provider_info = mgr.open_info(data_format)
124 if provider_info.batch:
125 paths = [_get_path(mgr, fi.file_name, provider_info.check_path)
126 for fi in file_infos]
127 models, status = collated_open(session, None, paths, data_format, _add_models,
128 opener_info.open, (session, paths, name), provider_kw)
129 if status:
130 statuses.append(status)
131 if models:
132 opened_models.append(name_and_group_models(models, name, paths))
133 else:
134 for fi in file_infos:
135 if provider_info.want_path:
136 data = _get_path(mgr, fi.file_name, provider_info.check_path)
137 else:
138 data = _get_stream(mgr, fi.file_name, data_format.encoding)
139 models, status = collated_open(session, None, [data], data_format, _add_models,
140 opener_info.open, (session, data,
141 name or model_name_from_path(fi.file_name)), provider_kw)
142 if status:
143 statuses.append(status)
144 if models:
145 opened_models.append(name_and_group_models(models, name,
146 [fi.file_name]))
147 else:
148 for fi in file_infos:
149 opener_info, provider_info = mgr.open_info(fi.data_format)
150 if provider_info.want_path:
151 data = _get_path(mgr, fi.file_name, provider_info.check_path)
152 else:
153 data = _get_stream(mgr, fi.file_name, fi.data_format.encoding)
154 models, status = collated_open(session, None, [data], fi.data_format, _add_models,
155 opener_info.open, (session, data, name or model_name_from_path(fi.file_name)), provider_kw)
156 if status:
157 statuses.append(status)
158 if models:
159 opened_models.append(name_and_group_models(models, name, [fi.file_name]))
160 for ident, database_name, format_name in fetches:
161 fetcher_info, default_format_name = _fetch_info(mgr, database_name, format)
162 if format_name is None:
163 format_name = default_format_name
164 models, status = collated_open(session, database_name, ident, session.data_formats[format_name],
165 _add_models, fetcher_info.fetch, (session, ident, format_name, ignore_cache), provider_kw)
166 if status:
167 statuses.append(status)
168 if models:
169 opened_models.append(name_and_group_models(models, name, [ident]))
170 if opened_models and _add_models:
171 session.models.add(opened_models)
172 if _add_models and len(names) == 1:
173 # TODO: Handle lists of file names in history
174 from chimerax.core.filehistory import remember_file
175 if fetches:
176 # Files opened in the help browser are done asynchronously and might have
177 # been misspelled and can't be deleted from file history. So skip them.
178 if not statuses or not statuses[-1].endswith(" in browser"):
179 remember_file(session, names[0], session.data_formats[format_name].nicknames[0],
180 opened_models or 'all models', database=database_name,
181 open_options=provider_kw)
182 else:
183 remember_file(session, names[0], file_infos[0].data_format.nicknames[0],
184 opened_models or 'all models', open_options=provider_kw)
185
186 status ='\n'.join(statuses) if statuses else ""
187 if _return_status:
188 return opened_models, status
189 elif status:
190 session.logger.status(status, log=True)
191 return opened_models
192
193def _fetch_info(mgr, database_name, default_format_name):
194 db_info = mgr.database_info(database_name)
195 from chimerax.core.commands import commas
196 if default_format_name:
197 try:
198 provider_info = db_info[default_format_name]
199 except KeyError:
200 raise UserError("Format '%s' not available for database '%s'. Available"
201 " formats are: %s" % (default_format_name, database_name,
202 commas(db_info.keys())))
203 else:
204 for default_format_name, provider_info in db_info.items():
205 if provider_info.is_default:
206 break
207 else:
208 raise UserError("No default format for database '%s'. Possible formats are:"
209 " %s" % (database_name, commas(db_info.keys())))
210 return (provider_info.bundle_info.run_provider(mgr.session, database_name, mgr),
211 default_format_name)
212
213def _get_path(mgr, file_name, check_path, check_compression=True):
214 from os.path import expanduser, expandvars, exists
215 expanded = expanduser(expandvars(file_name))
216 from chimerax.io import file_system_file_name
217 if check_path and not exists(file_system_file_name(expanded)):
218 raise UserError("No such file/path: %s" % file_name)
219
220 if check_compression:
221 from chimerax import io
222 if io.remove_compression_suffix(expanded) != expanded:
223 raise UserError("File reader requires uncompressed file; '%s' is compressed"
224 % file_name)
225 return expanded
226
227def _get_stream(mgr, file_name, encoding):
228 path = _get_path(mgr, file_name, True, check_compression=False)
229 from chimerax import io
230 return io.open_input(path, encoding)
231
232def fetches_vs_files(mgr, names, format_name, database_name):
233 fetches = []
234 files = []
235 from os.path import exists
236 for name in names:
237 if not database_name and exists(name):
238 print("no database and exists[1]")
239 files.append(name)
240 else:
241 f = fetch_info(mgr, name, format_name, database_name)
242 if f:
243 fetches.append(f)
244 else:
245 files.extend(expand_path(name))
246 return fetches, files
247
248def expand_path(file_name):
249 from os.path import exists
250 if exists(file_name):
251 return [file_name]
252
253 from glob import glob
254 file_names = glob(file_name)
255 if not file_names:
256 return [file_name]
257 # python glob does not sort. Keep series in order
258 file_names.sort()
259 return file_names
260
261def fetch_info(mgr, file_arg, format_name, database_name):
262 from os.path import exists
263 if not database_name and exists(file_arg):
264 print("no database and exists[2]")
265 return None
266 print("Likely PDB ID for '%s': %s" % (file_arg, likely_pdb_id(file_arg)))
267 if ':' in file_arg:
268 db_name, ident = file_arg.split(':', maxsplit=1)
269 elif database_name:
270 db_name = database_name
271 ident = file_arg
272 elif likely_pdb_id(file_arg):
273 db_name = "pdb"
274 ident = file_arg
275 else:
276 return None
277 from .manager import NoOpenerError
278 try:
279 db_formats = list(mgr.database_info(db_name).keys())
280 except NoOpenerError as e:
281 raise LimitationError(str(e))
282 if format_name and format_name not in db_formats:
283 # for backwards compatibiity, accept formal format name or nicknames
284 try:
285 df = mgr.session.data_formats[format_name]
286 except KeyError:
287 nicks = []
288 else:
289 nicks = df.nicknames + [df.name]
290 for nick in nicks:
291 if nick in db_formats:
292 format_name = nick
293 break
294 else:
295 from chimerax.core.commands import commas
296 raise UserError("Format '%s' not supported for database '%s'. Supported"
297 " formats are: %s" % (format_name, db_name,
298 commas([dbf for dbf in db_formats])))
299 return (ident, db_name, format_name)
300
301def name_and_group_models(models, name_arg, path_info):
302 if len(models) > 1:
303 # name arg only applies to group, not underlings
304 if name_arg:
305 names = [name_arg] * len(models)
306 elif len(path_info) == len(models):
307 names = [model_name_from_path(p) for p in path_info]
308 else:
309 names = [model_name_from_path(path_info[0])] * len(models)
310 for m, pn in zip(models, names):
311 if name_arg or not m.name:
312 m.name = pn
313 from chimerax.core.models import Model
314 names = set([m.name for m in models])
315 if len(names) == 1:
316 group_name = names.pop() + " group"
317 elif len(path_info) == 1:
318 group_name = model_name_from_path(path_info[0])
319 else:
320 group_name = "group"
321 group = Model(group_name, models[0].session)
322 group.add(models)
323 return group
324 model = models[0]
325 if name_arg:
326 model.name = name_arg
327 else:
328 if not model.name:
329 model.name = model_name_from_path(path_info[0])
330 return model
331
332def model_name_from_path(path):
333 from os.path import basename, dirname
334 name = basename(path)
335 if name.strip() == '':
336 # Path is a directory with trailing '/'. Use directory name.
337 name = basename(dirname(path))
338 return name
339
340def file_format(session, file_name, format_name):
341 if format_name:
342 try:
343 return session.data_formats[format_name]
344 except KeyError:
345 return None
346
347 from chimerax.data_formats import NoFormatError
348 try:
349 return session.data_formats.open_format_from_file_name(file_name)
350 except NoFormatError as e:
351 return None
352
353def collated_open(session, database_name, data, data_format, main_opener, func, func_args, func_kw):
354 is_script = data_format.category == session.data_formats.CAT_SCRIPT
355 if is_script:
356 with session.in_script:
357 return func(*func_args, **func_kw)
358 from chimerax.core.logger import Collator
359 if database_name:
360 description = "Summary of feedback from opening %s fetched from %s" % (data, database_name)
361 else:
362 if len(data) > 1:
363 opened_text = "files"
364 else:
365 if isinstance(data[0], str):
366 opened_text = data[0]
367 elif hasattr(data[0], 'name'):
368 opened_text = data[0].name
369 else:
370 opened_text = "input"
371 description = "Summary of feedback from opening %s" % opened_text
372 if main_opener:
373 with Collator(session.logger, description, True):
374 return func(*func_args, **func_kw)
375 return func(*func_args, **func_kw)
376
377class FileInfo:
378 def __init__(self, session, file_name, format_name):
379 self.file_name = file_name
380 self.data_format = file_format(session, file_name, format_name)
381 if self.data_format is None:
382 from os.path import splitext
383 from chimerax import io
384 ext = splitext(io.remove_compression_suffix(file_name))[1]
385 if ext:
386 raise UserError("Unrecognized file suffix '%s'" % ext)
387 raise UserError("'%s' has no suffix" % file_name)
388
389def cmd_open_formats(session):
390 '''Report file formats, suffixes and databases that the open command knows about.'''
391 if session.ui.is_gui:
392 lines = ['<table border=1 cellspacing=0 cellpadding=2>', '<tr><th>File format<th>Short name(s)<th>Suffixes']
393 else:
394 session.logger.info('File format, Short name(s), Suffixes:')
395 from chimerax.core.commands import commas
396 formats = session.open_command.open_data_formats
397 formats.sort(key = lambda f: f.name.lower())
398 for f in formats:
399 if session.ui.is_gui:
400 from html import escape
401 if f.reference_url:
402 descrip = '<a href="%s">%s</a>' % (f.reference_url, escape(f.synopsis))
403 else:
404 descrip = escape(f.synopsis)
405 lines.append('<tr><td>%s<td>%s<td>%s' % (descrip,
406 escape(commas(f.nicknames)), escape(', '.join(f.suffixes))))
407 else:
408 session.logger.info(' %s: %s: %s' % (f.synopsis,
409 commas(f.nicknames), ', '.join(f.suffixes)))
410 if session.ui.is_gui:
411 lines.append('</table>')
412 lines.append('<p></p>')
413
414 if session.ui.is_gui:
415 lines.extend(['<table border=1 cellspacing=0 cellpadding=2>', '<tr><th>Database<th>Formats'])
416 else:
417 session.logger.info('\nDatabase, Formats:')
418 database_names = session.open_command.database_names
419 database_names.sort(key=lambda dbn: dbn.lower())
420 for db_name in database_names:
421 db_info = session.open_command.database_info(db_name)
422 if 'web fetch' in db_info.keys() or db_name == 'help':
423 continue
424 for fmt_name, fetcher_info in db_info.items():
425 if fetcher_info.is_default:
426 default_name = session.data_formats[fmt_name].nicknames[0]
427 break
428 else:
429 continue
430 format_names = [session.data_formats[fmt_name].nicknames[0] for fmt_name in db_info.keys()]
431 format_names.sort()
432 format_names.remove(default_name)
433 format_names.insert(0, default_name)
434 if not session.ui.is_gui:
435 session.logger.info(' %s: %s' % (db_name, ', '.join(format_names)))
436 continue
437 line = '<tr><td>%s<td>%s' % (db_name, ', '.join(format_names))
438 lines.append(line)
439
440 if session.ui.is_gui:
441 lines.append('</table>')
442 msg = '\n'.join(lines)
443 session.logger.info(msg, is_html=True)
444
445
446def register_command(command_name, logger):
447 register('open', CmdDesc(required=[('file_names', OpenFileNamesArgNoRepeat),
448 ('rest_of_line', RestOfLine)], synopsis="Open/fetch data files",
449 self_logging=True), cmd_open, logger=logger)
450
451 of_desc = CmdDesc(synopsis='report formats that can be opened')
452 register('open formats', of_desc, cmd_open_formats, logger=logger)