Passed
Pull Request — develop (#301)
by
unknown
01:51
created

doorstop.cli.commands.run_list()   B

Complexity

Conditions 6

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

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