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

run_list()   B

Complexity

Conditions 6

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
c 0
b 0
f 0
dl 0
loc 25
rs 7.5384
ccs 10
cts 10
cp 1
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(is_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(is_auto_save=True, path=args.path, document=document, ext=ext, mapping=mapping)
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(is_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(is_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,
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,
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, is_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