1 | # SPDX-License-Identifier: LGPL-3.0-only |
||
2 | |||
3 | """Command functions.""" |
||
4 | |||
5 | import os |
||
6 | import time |
||
7 | from typing import Set |
||
8 | |||
9 | from doorstop import common, server |
||
10 | from doorstop.cli import utilities |
||
11 | from doorstop.core import editor, exporter, importer, publisher |
||
12 | from doorstop.core.builder import build |
||
13 | |||
14 | log = common.logger(__name__) |
||
15 | |||
16 | |||
17 | class CycleTracker: |
||
18 | """A cycle tracker to detect cyclic references between items. |
||
19 | |||
20 | The cycle tracker uses a standard algorithm to detect cycles in a directed |
||
21 | graph (not necessarily connected) using a depth first search with a bit of |
||
22 | graph colouring. The time complexity is O(|V| + |E|). The vertices are |
||
23 | the items. The edges are the links between items. |
||
24 | |||
25 | """ |
||
26 | |||
27 | def __init__(self): |
||
28 | """Initialize a cycle tracker.""" |
||
29 | self.discovered: Set[str] = set() |
||
30 | self.finished: Set[str] = set() |
||
31 | |||
32 | def _dfs_visit(self, uid, tree): |
||
33 | """Do a depth first search visit of the specified item. |
||
34 | |||
35 | :param uid: the UID of the item to visit |
||
36 | :param tree: the document hierarchy tree |
||
37 | |||
38 | :return: generator of :class:`~doorstop.common.DoorstopWarning` |
||
39 | |||
40 | """ |
||
41 | self.discovered.add(uid) |
||
42 | item = tree.find_item(uid) |
||
43 | |||
44 | for pid in item.links: |
||
45 | # Detect cycles via a back edge |
||
46 | if pid in self.discovered: |
||
47 | msg = "detected a cycle with a back edge from {} to {}".format(pid, uid) |
||
48 | yield common.DoorstopWarning(msg) |
||
49 | |||
50 | # Recurse, if this a fresh item |
||
51 | if pid not in self.discovered and pid not in self.finished: |
||
52 | yield from self._dfs_visit(pid, tree) |
||
53 | |||
54 | self.discovered.remove(uid) |
||
55 | self.finished.add(uid) |
||
56 | |||
57 | def __call__(self, item, document, tree): |
||
58 | """Get cycles which include the specified item. |
||
59 | |||
60 | :param item: the UID of the item to get the cycles for |
||
61 | :param document: unused |
||
62 | :param tree: the document hierarchy tree |
||
63 | |||
64 | :return: generator of :class:`~doorstop.common.DoorstopWarning` |
||
65 | |||
66 | """ |
||
67 | if item not in self.discovered and item not in self.finished: |
||
68 | yield from self._dfs_visit(item, tree) |
||
69 | |||
70 | |||
71 | def get(name): |
||
72 | """Get a command function by name.""" |
||
73 | if name: |
||
74 | log.debug("running command '{}'...".format(name)) |
||
75 | return globals()['run_' + name] |
||
76 | else: |
||
77 | log.debug("launching main command...") |
||
78 | return run |
||
79 | |||
80 | |||
81 | def run(args, cwd, error, catch=True): # pylint: disable=W0613 |
||
82 | """Process arguments and run the `doorstop` subcommand. |
||
83 | |||
84 | :param args: Namespace of CLI arguments |
||
85 | :param cwd: current working directory |
||
86 | :param error: function to call for CLI errors |
||
87 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
88 | |||
89 | """ |
||
90 | with utilities.capture(catch=catch) as success: |
||
91 | |||
92 | # get the tree |
||
93 | tree = _get_tree(args, cwd, load=True) |
||
94 | |||
95 | # validate it |
||
96 | utilities.show("validating items...", flush=True) |
||
97 | cycle_tracker = CycleTracker() |
||
98 | valid = tree.validate(skip=args.skip, item_hook=cycle_tracker) |
||
99 | |||
100 | if not success: |
||
101 | return False |
||
102 | |||
103 | if len(tree) > 1 and valid: |
||
104 | utilities.show('\n' + tree.draw() + '\n') |
||
105 | |||
106 | return valid |
||
107 | |||
108 | |||
109 | def run_create(args, cwd, _, catch=True): |
||
110 | """Process arguments and run the `doorstop create` subcommand. |
||
111 | |||
112 | :param args: Namespace of CLI arguments |
||
113 | :param cwd: current working directory |
||
114 | :param error: function to call for CLI errors |
||
115 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
116 | |||
117 | """ |
||
118 | with utilities.capture(catch=catch) as success: |
||
119 | |||
120 | # get the tree |
||
121 | tree = _get_tree(args, cwd) |
||
122 | |||
123 | # create a new document |
||
124 | document = tree.create_document( |
||
125 | args.path, |
||
126 | args.prefix, |
||
127 | parent=args.parent, |
||
128 | digits=args.digits, |
||
129 | sep=args.separator, |
||
130 | ) |
||
131 | |||
132 | if not success: |
||
133 | return False |
||
134 | |||
135 | utilities.show( |
||
136 | "created document: {} ({})".format(document.prefix, document.relpath) |
||
137 | ) |
||
138 | return True |
||
139 | |||
140 | |||
141 | def run_delete(args, cwd, _, catch=True): |
||
142 | """Process arguments and run the `doorstop delete` subcommand. |
||
143 | |||
144 | :param args: Namespace of CLI arguments |
||
145 | :param cwd: current working directory |
||
146 | :param error: function to call for CLI errors |
||
147 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
148 | |||
149 | """ |
||
150 | with utilities.capture(catch=catch) as success: |
||
151 | |||
152 | # get the document |
||
153 | tree = _get_tree(args, cwd) |
||
154 | document = tree.find_document(args.prefix) |
||
155 | |||
156 | # delete it |
||
157 | prefix, relpath = document.prefix, document.relpath |
||
158 | document.delete() |
||
159 | |||
160 | if not success: |
||
161 | return False |
||
162 | |||
163 | utilities.show("deleted document: {} ({})".format(prefix, relpath)) |
||
164 | |||
165 | return True |
||
166 | |||
167 | |||
168 | def run_add(args, cwd, _, catch=True): |
||
169 | """Process arguments and run the `doorstop add` subcommand. |
||
170 | |||
171 | :param args: Namespace of CLI arguments |
||
172 | :param cwd: current working directory |
||
173 | :param error: function to call for CLI errors |
||
174 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
175 | |||
176 | """ |
||
177 | with utilities.capture(catch=catch) as success: |
||
178 | |||
179 | # get the document |
||
180 | request_next_number = _request_next_number(args) |
||
181 | tree = _get_tree(args, cwd, request_next_number=request_next_number) |
||
182 | document = tree.find_document(args.prefix) |
||
183 | |||
184 | # add items to it |
||
185 | for _ in range(args.count): |
||
186 | item = document.add_item( |
||
187 | level=args.level, defaults=args.defaults, name=args.name |
||
188 | ) |
||
189 | utilities.show("added item: {} ({})".format(item.uid, item.relpath)) |
||
190 | |||
191 | # Edit item if requested |
||
192 | if args.edit: |
||
193 | item.edit(tool=args.tool) |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
194 | |||
195 | if not success: |
||
196 | return False |
||
197 | |||
198 | return True |
||
199 | |||
200 | |||
201 | def run_remove(args, cwd, _, catch=True): |
||
202 | """Process arguments and run the `doorstop remove` subcommand. |
||
203 | |||
204 | :param args: Namespace of CLI arguments |
||
205 | :param cwd: current working directory |
||
206 | :param error: function to call for CLI errors |
||
207 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
208 | |||
209 | """ |
||
210 | with utilities.capture(catch=catch) as success: |
||
211 | |||
212 | # get the item |
||
213 | tree = _get_tree(args, cwd) |
||
214 | item = tree.find_item(args.uid) |
||
215 | |||
216 | # delete it |
||
217 | item.delete() |
||
218 | |||
219 | if not success: |
||
220 | return False |
||
221 | |||
222 | utilities.show("removed item: {} ({})".format(item.uid, item.relpath)) |
||
223 | |||
224 | return True |
||
225 | |||
226 | |||
227 | def run_edit(args, cwd, error, catch=True): |
||
228 | """Process arguments and run the `doorstop edit` subcommand. |
||
229 | |||
230 | :param args: Namespace of CLI arguments |
||
231 | :param cwd: current working directory |
||
232 | :param error: function to call for CLI errors |
||
233 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
234 | |||
235 | """ |
||
236 | item = document = None |
||
237 | ext = utilities.get_ext(args, error, '.yml', '.yml', whole_tree=False) |
||
238 | |||
239 | with utilities.capture(catch=catch) as success: |
||
240 | |||
241 | # get the item or document |
||
242 | request_next_number = _request_next_number(args) |
||
243 | tree = _get_tree(args, cwd, request_next_number=request_next_number) |
||
244 | if not args.document: |
||
245 | try: |
||
246 | item = tree.find_item(args.label) |
||
247 | except common.DoorstopError as exc: |
||
248 | if args.item: |
||
249 | raise exc from None # pylint: disable=raising-bad-type |
||
250 | if not item: |
||
251 | document = tree.find_document(args.label) |
||
252 | |||
253 | # edit it |
||
254 | if item: |
||
255 | item.edit(tool=args.tool, edit_all=args.all) |
||
256 | else: |
||
257 | _export_import(args, cwd, error, document, ext) |
||
258 | |||
259 | if not success: |
||
260 | return False |
||
261 | |||
262 | if item: |
||
263 | utilities.show("opened item: {} ({})".format(item.uid, item.relpath)) |
||
264 | |||
265 | return True |
||
266 | |||
267 | |||
268 | def run_reorder(args, cwd, error, catch=True, _tree=None): |
||
269 | """Process arguments and run the `doorstop reorder` subcommand. |
||
270 | |||
271 | :param args: Namespace of CLI arguments |
||
272 | :param cwd: current working directory |
||
273 | :param error: function to call for CLI errors |
||
274 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
275 | |||
276 | """ |
||
277 | reordered = False |
||
278 | |||
279 | with utilities.capture(catch=catch) as success: |
||
280 | |||
281 | # get the document |
||
282 | tree = _tree or _get_tree(args, cwd) |
||
283 | document = tree.find_document(args.prefix) |
||
284 | |||
285 | if not success: |
||
286 | return False |
||
287 | |||
288 | with utilities.capture(catch=catch) as success: |
||
289 | |||
290 | # automatically order |
||
291 | if args.auto: |
||
292 | msg = "reordering document {}...".format(document) |
||
293 | utilities.show(msg, flush=True) |
||
294 | document.reorder(manual=False) |
||
295 | reordered = True |
||
296 | |||
297 | # or, reorder from a previously updated index |
||
298 | elif document.index: |
||
299 | relpath = os.path.relpath(document.index, cwd) |
||
300 | if utilities.ask("reorder from '{}'?".format(relpath)): |
||
301 | msg = "reordering document {}...".format(document) |
||
302 | utilities.show(msg, flush=True) |
||
303 | document.reorder(automatic=not args.manual) |
||
304 | reordered = True |
||
305 | else: |
||
306 | del document.index |
||
307 | |||
308 | # or, create a new index to update |
||
309 | else: |
||
310 | document.index = True # create index |
||
311 | relpath = os.path.relpath(document.index, cwd) |
||
312 | editor.edit(relpath, tool=args.tool) |
||
313 | get('reorder')(args, cwd, error, catch=False, _tree=tree) |
||
314 | |||
315 | if not success: |
||
316 | msg = "after fixing the error: doorstop reorder {}".format(document) |
||
317 | utilities.show(msg) |
||
318 | return False |
||
319 | |||
320 | if reordered: |
||
321 | utilities.show("reordered document: {}".format(document)) |
||
322 | |||
323 | return True |
||
324 | |||
325 | |||
326 | def run_link(args, cwd, _, catch=True): |
||
327 | """Process arguments and run the `doorstop link` subcommand. |
||
328 | |||
329 | :param args: Namespace of CLI arguments |
||
330 | :param cwd: current working directory |
||
331 | :param error: function to call for CLI errors |
||
332 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
333 | |||
334 | """ |
||
335 | with utilities.capture(catch=catch) as success: |
||
336 | |||
337 | # get the tree |
||
338 | tree = _get_tree(args, cwd) |
||
339 | |||
340 | # link items |
||
341 | child, parent = tree.link_items(args.child, args.parent) |
||
342 | |||
343 | if not success: |
||
344 | return False |
||
345 | |||
346 | msg = "linked items: {} ({}) -> {} ({})".format( |
||
347 | child.uid, child.relpath, parent.uid, parent.relpath |
||
348 | ) |
||
349 | utilities.show(msg) |
||
350 | |||
351 | return True |
||
352 | |||
353 | |||
354 | def run_unlink(args, cwd, _, catch=True): |
||
355 | """Process arguments and run the `doorstop unlink` subcommand. |
||
356 | |||
357 | :param args: Namespace of CLI arguments |
||
358 | :param cwd: current working directory |
||
359 | :param error: function to call for CLI errors |
||
360 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
361 | |||
362 | """ |
||
363 | with utilities.capture(catch=catch) as success: |
||
364 | |||
365 | # get the tree |
||
366 | tree = _get_tree(args, cwd) |
||
367 | |||
368 | # unlink items |
||
369 | child, parent = tree.unlink_items(args.child, args.parent) |
||
370 | |||
371 | if not success: |
||
372 | return False |
||
373 | |||
374 | msg = "unlinked items: {} ({}) -> {} ({})".format( |
||
375 | child.uid, child.relpath, parent.uid, parent.relpath |
||
376 | ) |
||
377 | utilities.show(msg) |
||
378 | |||
379 | return True |
||
380 | |||
381 | |||
382 | def run_clear(args, cwd, error, catch=True): |
||
383 | """Process arguments and run the `doorstop clear` subcommand. |
||
384 | |||
385 | :param args: Namespace of CLI arguments |
||
386 | :param cwd: current working directory |
||
387 | :param error: function to call for CLI errors |
||
388 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
389 | |||
390 | """ |
||
391 | with utilities.capture(catch=catch) as success: |
||
392 | tree = _get_tree(args, cwd) |
||
393 | |||
394 | if args.parents: |
||
395 | # Check that the parent item UIDs exist |
||
396 | for pid in args.parents: |
||
397 | tree.find_item(pid) |
||
398 | |||
399 | pids = " to " + ", ".join(args.parents) |
||
400 | else: |
||
401 | pids = "" |
||
402 | |||
403 | for item in _iter_items(args, tree, error): |
||
404 | msg = "clearing item {}'s suspect links{}...".format(item.uid, pids) |
||
405 | utilities.show(msg) |
||
406 | item.clear(parents=args.parents) |
||
407 | |||
408 | if not success: |
||
409 | return False |
||
410 | |||
411 | return True |
||
412 | |||
413 | |||
414 | def run_review(args, cwd, error, catch=True): |
||
415 | """Process arguments and run the `doorstop review` subcommand. |
||
416 | |||
417 | :param args: Namespace of CLI arguments |
||
418 | :param cwd: current working directory |
||
419 | :param error: function to call for CLI errors |
||
420 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
421 | |||
422 | """ |
||
423 | with utilities.capture(catch=catch) as success: |
||
424 | tree = _get_tree(args, cwd) |
||
425 | |||
426 | for item in _iter_items(args, tree, error): |
||
427 | utilities.show("marking item {} as reviewed...".format(item.uid)) |
||
428 | item.review() |
||
429 | |||
430 | if not success: |
||
431 | return False |
||
432 | |||
433 | return True |
||
434 | |||
435 | |||
436 | def run_import(args, cwd, error, catch=True, _tree=None): |
||
437 | """Process arguments and run the `doorstop import` subcommand. |
||
438 | |||
439 | :param args: Namespace of CLI arguments |
||
440 | :param cwd: current working directory |
||
441 | :param error: function to call for CLI errors |
||
442 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
443 | |||
444 | """ |
||
445 | document = item = None |
||
446 | attrs = utilities.literal_eval(args.attrs, error) |
||
447 | mapping = utilities.literal_eval(args.map, error) |
||
448 | if args.path: |
||
449 | if not args.prefix: |
||
450 | error("when [path] specified, [prefix] is also required") |
||
451 | elif args.document: |
||
452 | error("'--document' cannot be used with [path] [prefix]") |
||
453 | elif args.item: |
||
454 | error("'--item' cannot be used with [path] [prefix]") |
||
455 | ext = utilities.get_ext(args, error, None, None) |
||
456 | elif not (args.document or args.item): |
||
457 | error("specify [path], '--document', or '--item' to import") |
||
458 | |||
459 | with utilities.capture(catch=catch) as success: |
||
460 | |||
461 | if args.path: |
||
462 | |||
463 | # get the document |
||
464 | request_next_number = _request_next_number(args) |
||
465 | tree = _tree or _get_tree( |
||
466 | args, cwd, request_next_number=request_next_number |
||
467 | ) |
||
468 | document = tree.find_document(args.prefix) |
||
469 | |||
470 | # import items into it |
||
471 | msg = "importing '{}' into document {}...".format(args.path, document) |
||
472 | utilities.show(msg, flush=True) |
||
473 | importer.import_file(args.path, document, ext, mapping=mapping) |
||
0 ignored issues
–
show
|
|||
474 | |||
475 | elif args.document: |
||
476 | prefix, path = args.document |
||
477 | document = importer.create_document(prefix, path, parent=args.parent) |
||
478 | elif args.item: |
||
479 | prefix, uid = args.item |
||
480 | request_next_number = _request_next_number(args) |
||
481 | item = importer.add_item( |
||
482 | prefix, uid, attrs=attrs, request_next_number=request_next_number |
||
483 | ) |
||
484 | if not success: |
||
485 | return False |
||
486 | |||
487 | if document: |
||
488 | utilities.show( |
||
489 | "imported document: {} ({})".format(document.prefix, document.relpath) |
||
490 | ) |
||
491 | else: |
||
492 | assert item |
||
493 | utilities.show("imported item: {} ({})".format(item.uid, item.relpath)) |
||
494 | |||
495 | return True |
||
496 | |||
497 | |||
498 | def run_export(args, cwd, error, catch=True, auto=False, _tree=None): |
||
499 | """Process arguments and run the `doorstop export` subcommand. |
||
500 | |||
501 | :param args: Namespace of CLI arguments |
||
502 | :param cwd: current working directory |
||
503 | :param error: function to call for CLI errors |
||
504 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
505 | |||
506 | :param auto: include placeholders for new items on import |
||
507 | |||
508 | """ |
||
509 | whole_tree = args.prefix == 'all' |
||
510 | ext = utilities.get_ext(args, error, '.yml', '.csv', whole_tree=whole_tree) |
||
511 | |||
512 | # Get the tree or document |
||
513 | with utilities.capture(catch=catch) as success: |
||
514 | |||
515 | exporter.check(ext) |
||
516 | tree = _tree or _get_tree(args, cwd, load=whole_tree) |
||
517 | if not whole_tree: |
||
518 | document = tree.find_document(args.prefix) |
||
519 | |||
520 | if not success: |
||
521 | return False |
||
522 | |||
523 | # Write to output file(s) |
||
524 | if args.path: |
||
525 | if whole_tree: |
||
526 | msg = "exporting tree to '{}'...".format(args.path) |
||
527 | utilities.show(msg, flush=True) |
||
528 | path = exporter.export(tree, args.path, ext, auto=auto) |
||
529 | else: |
||
530 | msg = "exporting document {} to '{}'...".format(document, args.path) |
||
0 ignored issues
–
show
|
|||
531 | utilities.show(msg, flush=True) |
||
532 | path = exporter.export(document, args.path, ext, auto=auto) |
||
533 | if path: |
||
534 | utilities.show("exported: {}".format(path)) |
||
535 | |||
536 | # Or, display to standard output |
||
537 | else: |
||
538 | if whole_tree: |
||
539 | error("only single documents can be displayed") |
||
540 | for line in exporter.export_lines(document, ext): |
||
541 | utilities.show(line) |
||
542 | |||
543 | return True |
||
544 | |||
545 | |||
546 | def run_publish(args, cwd, error, catch=True): |
||
547 | """Process arguments and run the `doorstop publish` subcommand. |
||
548 | |||
549 | :param args: Namespace of CLI arguments |
||
550 | :param cwd: current working directory |
||
551 | :param error: function to call for CLI errors |
||
552 | :param catch: catch and log :class:`~doorstop.common.DoorstopError` |
||
553 | |||
554 | """ |
||
555 | whole_tree = args.prefix == 'all' |
||
556 | ext = utilities.get_ext(args, error, '.txt', '.html', whole_tree) |
||
557 | |||
558 | # Get the tree or document |
||
559 | with utilities.capture(catch=catch) as success: |
||
560 | |||
561 | publisher.check(ext) |
||
562 | tree = _get_tree(args, cwd, load=whole_tree) |
||
563 | if not whole_tree: |
||
564 | document = tree.find_document(args.prefix) |
||
565 | |||
566 | if not success: |
||
567 | return False |
||
568 | |||
569 | # Set publishing arguments |
||
570 | kwargs = {} |
||
571 | if args.width: |
||
572 | kwargs['width'] = args.width |
||
573 | |||
574 | # Write to output file(s) |
||
575 | if args.path: |
||
576 | path = os.path.abspath(os.path.join(cwd, args.path)) |
||
577 | if whole_tree: |
||
578 | msg = "publishing tree to '{}'...".format(path) |
||
579 | utilities.show(msg, flush=True) |
||
580 | published_path = publisher.publish( |
||
581 | tree, path, ext, template=args.template, **kwargs |
||
582 | ) |
||
583 | else: |
||
584 | msg = "publishing document {} to '{}'...".format(document, path) |
||
0 ignored issues
–
show
|
|||
585 | utilities.show(msg, flush=True) |
||
586 | published_path = publisher.publish( |
||
587 | document, path, ext, template=args.template, **kwargs |
||
588 | ) |
||
589 | if published_path: |
||
590 | utilities.show("published: {}".format(published_path)) |
||
591 | |||
592 | # Or, display to standard output |
||
593 | else: |
||
594 | if whole_tree: |
||
595 | error("only single documents can be displayed") |
||
596 | for line in publisher.publish_lines(document, ext, **kwargs): |
||
597 | utilities.show(line) |
||
598 | |||
599 | return True |
||
600 | |||
601 | |||
602 | def _request_next_number(args): |
||
603 | """Get the server's "next number" method if a server exists.""" |
||
604 | if args.force: |
||
605 | log.warning("creating items without the server...") |
||
606 | return None |
||
607 | else: |
||
608 | server.check() |
||
609 | return server.get_next_number |
||
610 | |||
611 | |||
612 | def _get_tree(args, cwd, request_next_number=None, load=False): |
||
613 | """Build a tree and optionally load all documents. |
||
614 | |||
615 | :param args: Namespace of CLI arguments |
||
616 | :param cwd: current working directory |
||
617 | :param request_next_number: server method to get a document's next number |
||
618 | :param load: force the early loading of all documents |
||
619 | |||
620 | :return: built :class:`~doorstop.core.tree.Tree` |
||
621 | |||
622 | """ |
||
623 | utilities.show("building tree...", flush=True) |
||
624 | tree = build(cwd=cwd, root=args.project, request_next_number=request_next_number) |
||
625 | |||
626 | if load: |
||
627 | utilities.show("loading documents...", flush=True) |
||
628 | tree.load() |
||
629 | |||
630 | return tree |
||
631 | |||
632 | |||
633 | def _iter_items(args, tree, error): |
||
634 | """Iterate through items. |
||
635 | |||
636 | :param args: Namespace of CLI arguments |
||
637 | :param tree: the document hierarchy tree |
||
638 | :param error: function to call for CLI errors |
||
639 | |||
640 | Items are filtered to: |
||
641 | |||
642 | - `args.label` == 'all': all items |
||
643 | - `args.label` == document prefix: the document's items |
||
644 | - `args.label` == item UID: a single item |
||
645 | |||
646 | Documents and items are inferred unless flagged by: |
||
647 | |||
648 | - `args.document`: `args.label` is a prefix |
||
649 | - `args.item`: `args.label` is an UID |
||
650 | |||
651 | """ |
||
652 | # Parse arguments |
||
653 | if args.label == 'all': |
||
654 | if args.item: |
||
655 | error("argument -i/--item: not allowed with 'all'") |
||
656 | if args.document: |
||
657 | error("argument -d/--document: not allowed with 'all'") |
||
658 | |||
659 | # Build tree |
||
660 | item = None |
||
661 | document = None |
||
662 | |||
663 | # Determine if tree, document, or item was requested |
||
664 | if args.label != 'all': |
||
665 | if not args.item: |
||
666 | try: |
||
667 | document = tree.find_document(args.label) |
||
668 | except common.DoorstopError as exc: |
||
669 | if args.document: |
||
670 | raise exc from None # pylint: disable=raising-bad-type |
||
671 | if not document: |
||
672 | item = tree.find_item(args.label) |
||
673 | |||
674 | # Yield items from the requested object |
||
675 | if item: |
||
676 | yield item |
||
677 | elif document: |
||
678 | for item in document: |
||
679 | yield item |
||
680 | else: |
||
681 | for document in tree: |
||
682 | for item in document: |
||
683 | yield item |
||
684 | |||
685 | |||
686 | def _export_import(args, cwd, error, document, ext): |
||
687 | """Edit a document by calling export followed by import. |
||
688 | |||
689 | :param args: Namespace of CLI arguments |
||
690 | :param cwd: current working directory |
||
691 | :param error: function to call for CLI errors |
||
692 | :param document: :class:`~doorstop.core.document.Document` to edit |
||
693 | :param ext: extension for export format |
||
694 | |||
695 | """ |
||
696 | # Export the document to file |
||
697 | args.prefix = document.prefix |
||
698 | path = "{}-{}{}".format(args.prefix, int(time.time()), ext) |
||
699 | args.path = path |
||
700 | get('export')(args, cwd, error, catch=False, auto=True, _tree=document.tree) |
||
701 | |||
702 | # Open the exported file |
||
703 | editor.edit(path, tool=args.tool) |
||
704 | |||
705 | # Import the file to the same document |
||
706 | if utilities.ask("import from '{}'?".format(path)): |
||
707 | args.attrs = {} |
||
708 | args.map = {} |
||
709 | get('import')(args, cwd, error, catch=False, _tree=document.tree) |
||
710 | common.delete(path) |
||
711 | else: |
||
712 | utilities.show("import canceled") |
||
713 | if utilities.ask("delete '{}'?".format(path)): |
||
714 | common.delete(path) |
||
715 | else: |
||
716 | msg = "to manually import: doorstop import {0}".format(path) |
||
717 | utilities.show(msg) |
||
718 |