Completed
Pull Request — develop (#227)
by
unknown
03:08
created

run_publish()   C

Complexity

Conditions 10

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
c 1
b 0
f 0
dl 0
loc 54
rs 5.1428
ccs 29
cts 29
cp 1
crap 10

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like run_publish() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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))
0 ignored issues
show
introduced by
Use formatting in logging functions but pass the parameters as arguments
Loading history...
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(args.path, 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_delete(args, cwd, _, catch=True):
79
    """Process arguments and run the `doorstop delete` 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
        # get the document
90 1
        tree = _get_tree(args, cwd)
91 1
        document = tree.find_document(args.prefix)
92
93
        # delete it
94 1
        prefix, relpath = document.prefix, document.relpath
95 1
        document.delete()
96
97 1
    if not success:
98 1
        return False
99
100 1
    utilities.show("deleted document: {} ({})".format(prefix, relpath))
101
102 1
    return True
103
104
105 1
def run_add(args, cwd, _, catch=True):
106
    """Process arguments and run the `doorstop add` 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
        request_next_number = _request_next_number(args)
118 1
        tree = _get_tree(args, cwd, request_next_number=request_next_number)
119 1
        document = tree.find_document(args.prefix)
120
121
        # add items to it
122 1
        for _ in range(args.count):
123 1
            item = document.add_item(level=args.level)
124 1
            utilities.show("added item: {} ({})".format(item.uid,
125
                                                        item.relpath))
126
127 1
    if not success:
128 1
        return False
129
130 1
    return True
131
132
133 1
def run_remove(args, cwd, _, catch=True):
134
    """Process arguments and run the `doorstop remove` subcommand.
135
136
    :param args: Namespace of CLI arguments
137
    :param cwd: current working directory
138
    :param error: function to call for CLI errors
139
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
140
141
    """
142 1
    with utilities.capture(catch=catch) as success:
143
144
        # get the item
145 1
        tree = _get_tree(args, cwd)
146 1
        item = tree.find_item(args.uid)
147
148
        # delete it
149 1
        item.delete()
150
151 1
    if not success:
152 1
        return False
153
154 1
    utilities.show("removed item: {} ({})".format(item.uid, item.relpath))
155
156 1
    return True
157
158
159 1
def run_edit(args, cwd, error, catch=True):
160
    """Process arguments and run the `doorstop edit` subcommand.
161
162
    :param args: Namespace of CLI arguments
163
    :param cwd: current working directory
164
    :param error: function to call for CLI errors
165
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
166
167
    """
168 1
    item = document = None
169 1
    ext = utilities.get_ext(args, error, '.yml', '.yml', whole_tree=False)
170
171 1
    with utilities.capture(catch=catch) as success:
172
173
        # get the item or document
174 1
        request_next_number = _request_next_number(args)
175 1
        tree = _get_tree(args, cwd, request_next_number=request_next_number)
176 1
        if not args.document:
177 1
            try:
178 1
                item = tree.find_item(args.label)
179 1
            except common.DoorstopError as exc:
180 1
                if args.item:
181 1
                    raise exc from None
182 1
        if not item:
183 1
            document = tree.find_document(args.label)
184
185
        # edit it
186 1
        if item:
187 1
            item.edit(tool=args.tool)
188
        else:
189 1
            _export_import(args, cwd, error, document, ext)
190
191 1
    if not success:
192 1
        return False
193
194 1
    if item:
195 1
        utilities.show("opened item: {} ({})".format(item.uid, item.relpath))
196
197 1
    return True
198
199
200 1
def run_reorder(args, cwd, error, catch=True, _tree=None):
201
    """Process arguments and run the `doorstop reorder` subcommand.
202
203
    :param args: Namespace of CLI arguments
204
    :param cwd: current working directory
205
    :param error: function to call for CLI errors
206
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
207
208
    """
209 1
    reordered = False
210
211 1
    with utilities.capture(catch=catch) as success:
212
213
        # get the document
214 1
        tree = _tree or _get_tree(args, cwd)
215 1
        document = tree.find_document(args.prefix)
216
217 1
    if not success:
218 1
        return False
219
220 1
    with utilities.capture(catch=catch) as success:
221
222
        # automatically order
223 1
        if args.auto:
224 1
            msg = "reordering document {}...".format(document)
225 1
            utilities.show(msg, flush=True)
226 1
            document.reorder(manual=False)
227 1
            reordered = True
228
229
        # or, reorder from a previously updated index
230 1
        elif document.index:
231 1
            relpath = os.path.relpath(document.index, cwd)
232 1
            if utilities.ask("reorder from '{}'?".format(relpath)):
233 1
                msg = "reordering document {}...".format(document)
234 1
                utilities.show(msg, flush=True)
235 1
                document.reorder(automatic=not args.manual)
236 1
                reordered = True
237
            else:
238 1
                del document.index
239
240
        # or, create a new index to update
241
        else:
242 1
            document.index = True  # create index
243 1
            relpath = os.path.relpath(document.index, cwd)
244 1
            editor.edit(relpath, tool=args.tool)
245 1
            get('reorder')(args, cwd, error, catch=False, _tree=tree)
246
247 1
    if not success:
248 1
        msg = "after fixing the error: doorstop reorder {}".format(document)
249 1
        utilities.show(msg)
250 1
        return False
251
252 1
    if reordered:
253 1
        utilities.show("reordered document: {}".format(document))
254
255 1
    return True
256
257
258 1
def run_link(args, cwd, _, catch=True):
259
    """Process arguments and run the `doorstop link` subcommand.
260
261
    :param args: Namespace of CLI arguments
262
    :param cwd: current working directory
263
    :param error: function to call for CLI errors
264
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
265
266
    """
267 1
    with utilities.capture(catch=catch) as success:
268
269
        # get the tree
270 1
        tree = _get_tree(args, cwd)
271
272
        # link items
273 1
        child, parent = tree.link_items(args.child, args.parent)
274
275 1
    if not success:
276 1
        return False
277
278 1
    msg = "linked items: {} ({}) -> {} ({})".format(child.uid,
279
                                                    child.relpath,
280
                                                    parent.uid,
281
                                                    parent.relpath)
282 1
    utilities.show(msg)
283
284 1
    return True
285
286
287 1
def run_unlink(args, cwd, _, catch=True):
288
    """Process arguments and run the `doorstop unlink` subcommand.
289
290
    :param args: Namespace of CLI arguments
291
    :param cwd: current working directory
292
    :param error: function to call for CLI errors
293
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
294
295
    """
296 1
    with utilities.capture(catch=catch) as success:
297
298
        # get the tree
299 1
        tree = _get_tree(args, cwd)
300
301
        # unlink items
302 1
        child, parent = tree.unlink_items(args.child, args.parent)
303
304 1
    if not success:
305 1
        return False
306
307 1
    msg = "unlinked items: {} ({}) -> {} ({})".format(child.uid,
308
                                                      child.relpath,
309
                                                      parent.uid,
310
                                                      parent.relpath)
311 1
    utilities.show(msg)
312
313 1
    return True
314
315
316 1
def run_clear(args, cwd, error, catch=True):
317
    """Process arguments and run the `doorstop clear` subcommand.
318
319
    :param args: Namespace of CLI arguments
320
    :param cwd: current working directory
321
    :param error: function to call for CLI errors
322
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
323
324
    """
325 1
    with utilities.capture(catch=catch) as success:
326
327 1
        for item in _iter_items(args, cwd, error):
328 1
            msg = "clearing item {}'s suspect links...".format(item.uid)
329 1
            utilities.show(msg)
330 1
            item.clear()
331
332 1
    if not success:
333 1
        return False
334
335 1
    return True
336
337
338 1
def run_review(args, cwd, error, catch=True):
339
    """Process arguments and run the `doorstop review` subcommand.
340
341
    :param args: Namespace of CLI arguments
342
    :param cwd: current working directory
343
    :param error: function to call for CLI errors
344
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
345
346
    """
347 1
    with utilities.capture(catch=catch) as success:
348
349 1
        for item in _iter_items(args, cwd, error):
350 1
            utilities.show("marking item {} as reviewed...".format(item.uid))
351 1
            item.review()
352
353 1
    if not success:
354 1
        return False
355
356 1
    return True
357
358
359 1
def run_import(args, cwd, error, catch=True, _tree=None):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (16/15).
Loading history...
360
    """Process arguments and run the `doorstop import` subcommand.
361
362
    :param args: Namespace of CLI arguments
363
    :param cwd: current working directory
364
    :param error: function to call for CLI errors
365
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
366
367
    """
368 1
    document = item = None
369 1
    attrs = utilities.literal_eval(args.attrs, error)
370 1
    mapping = utilities.literal_eval(args.map, error)
371 1
    if args.path:
372 1
        if not args.prefix:
373 1
            error("when [path] specified, [prefix] is also required")
374 1
        elif args.document:
375 1
            error("'--document' cannot be used with [path] [prefix]")
376 1
        elif args.item:
377 1
            error("'--item' cannot be used with [path] [prefix]")
378 1
        ext = utilities.get_ext(args, error, None, None)
379 1
    elif not (args.document or args.item):
380 1
        error("specify [path], '--document', or '--item' to import")
381
382 1
    with utilities.capture(catch=catch) as success:
383
384 1
        if args.path:
385
386
            # get the document
387 1
            request_next_number = _request_next_number(args)
388 1
            tree = _tree or _get_tree(args, cwd,
389
                                      request_next_number=request_next_number)
390 1
            document = tree.find_document(args.prefix)
391
392
            # import items into it
393 1
            msg = "importing '{}' into document {}...".format(args.path,
394
                                                              document)
395 1
            utilities.show(msg, flush=True)
396 1
            importer.import_file(args.path, document, ext, mapping=mapping)
397
398 1
        elif args.document:
399 1
            prefix, path = args.document
400 1
            document = importer.create_document(prefix, path,
401
                                                parent=args.parent)
402 1
        elif args.item:
403 1
            prefix, uid = args.item
404 1
            request_next_number = _request_next_number(args)
405 1
            item = importer.add_item(prefix, uid, attrs=attrs,
406
                                     request_next_number=request_next_number)
407 1
    if not success:
408 1
        return False
409
410 1
    if document:
411 1
        utilities.show("imported document: {} ({})".format(document.prefix,
412
                                                           document.relpath))
413
    else:
414 1
        assert item
415 1
        utilities.show("imported item: {} ({})".format(item.uid, item.relpath))
416
417 1
    return True
418
419
420 1
def run_export(args, cwd, error, catch=True, auto=False, _tree=None):
421
    """Process arguments and run the `doorstop export` subcommand.
422
423
    :param args: Namespace of CLI arguments
424
    :param cwd: current working directory
425
    :param error: function to call for CLI errors
426
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
427
428
    :param auto: include placeholders for new items on import
429
430
    """
431 1
    whole_tree = args.prefix == 'all'
432 1
    ext = utilities.get_ext(args, error, '.yml', '.csv', whole_tree=whole_tree)
433
434
    # Get the tree or document
435 1
    with utilities.capture(catch=catch) as success:
436
437 1
        exporter.check(ext)
438 1
        tree = _tree or _get_tree(args, cwd, load=whole_tree)
439 1
        if not whole_tree:
440 1
            document = tree.find_document(args.prefix)
441
442 1
    if not success:
443 1
        return False
444
445
    # Write to output file(s)
446 1
    if args.path:
447 1
        if whole_tree:
448 1
            msg = "exporting tree to '{}'...".format(args.path)
449 1
            utilities.show(msg, flush=True)
450 1
            path = exporter.export(tree, args.path, ext, auto=auto)
451
        else:
452 1
            msg = "exporting document {} to '{}'...".format(document,
453
                                                            args.path)
454 1
            utilities.show(msg, flush=True)
455 1
            path = exporter.export(document, args.path, ext, auto=auto)
456 1
        if path:
457 1
            utilities.show("exported: {}".format(path))
458
459
    # Or, display to standard output
460
    else:
461 1
        if whole_tree:
462 1
            error("only single documents can be displayed")
463 1
        for line in exporter.export_lines(document, ext):
464 1
            utilities.show(line)
465
466 1
    return True
467
468
469 1
def run_publish(args, cwd, error, catch=True):
470
    """Process arguments and run the `doorstop publish` subcommand.
471
472
    :param args: Namespace of CLI arguments
473
    :param cwd: current working directory
474
    :param error: function to call for CLI errors
475
    :param catch: catch and log :class:`~doorstop.common.DoorstopError`
476
477
    """
478 1
    whole_tree = args.prefix == 'all'
479 1
    ext = utilities.get_ext(args, error, '.txt', '.html', whole_tree)
480
481
    # Get the tree or document
482 1
    with utilities.capture(catch=catch) as success:
483
484 1
        publisher.check(ext)
485 1
        tree = _get_tree(args, cwd, load=whole_tree)
486 1
        if not whole_tree:
487 1
            document = tree.find_document(args.prefix)
488
489 1
    if not success:
490 1
        return False
491
492
    # Set publishing arguments
493 1
    kwargs = {}
494 1
    if args.width:
495 1
        kwargs['width'] = args.width
496
497
    # Write to output file(s)
498 1
    if args.path:
499 1
        path = os.path.abspath(os.path.join(cwd, args.path))
500 1
        if whole_tree:
501 1
            msg = "publishing tree to '{}'...".format(path)
502 1
            utilities.show(msg, flush=True)
503 1
            published_path = publisher.publish(tree, path, ext, template=args.template,
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (87/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
504
                                               toc=args.toc, **kwargs)
505
        else:
506 1
            msg = "publishing document {} to '{}'...".format(document,
507
                                                             path)
508 1
            utilities.show(msg, flush=True)
509 1
            published_path = publisher.publish(document, path, ext, template=args.template,
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (91/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
510
                                               toc=args.toc, **kwargs)
511 1
        if published_path:
512 1
            utilities.show("published: {}".format(published_path))
513
514
    # Or, display to standard output
515
    else:
516 1
        if whole_tree:
517 1
            error("only single documents can be displayed")
518 1
        for line in publisher.publish_lines(document, ext, template=args.template,
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (82/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
519
                                            toc=args.toc, **kwargs):
520 1
            utilities.show(line)
521
522 1
    return True
523
524
525 1
def _request_next_number(args):
526
    """Get the server's "next number" method if a server exists."""
527 1
    if args.force:
528 1
        log.warn("creating items without the server...")
529 1
        return None
530
    else:
531 1
        server.check()
532 1
        return server.get_next_number
533
534
535 1
def _get_tree(args, cwd, request_next_number=None, load=False):
536
    """Build a tree and optionally load all documents.
537
538
    :param args: Namespace of CLI arguments
539
    :param cwd: current working directory
540
    :param request_next_number: server method to get a document's next number
541
    :param load: force the early loading of all documents
542
543
    :return: built :class:`~doorstop.core.tree.Tree`
544
545
    """
546 1
    utilities.show("building tree...", flush=True)
547 1
    tree = build(cwd=cwd, root=args.project,
548
                 request_next_number=request_next_number)
549
550 1
    if load:
551 1
        utilities.show("loading documents...", flush=True)
552 1
        tree.load()
553
554 1
    return tree
555
556
557 1
def _iter_items(args, cwd, error):
558
    """Build a tree and iterate through items.
559
560
    :param args: Namespace of CLI arguments
561
    :param cwd: current working directory
562
    :param error: function to call for CLI errors
563
564
    Items are filtered to:
565
566
    - `args.label` == 'all': all items
567
    - `args.label` == document prefix: the document's items
568
    - `args.label` == item UID: a single item
569
570
    Documents and items are inferred unless flagged by:
571
572
    - `args.document`: `args.label` is a prefix
573
    - `args.item`: `args.label` is an UID
574
575
    """
576
    # Parse arguments
577 1
    if args.label == 'all':
578 1
        if args.item:
579 1
            error("argument -i/--item: not allowed with 'all'")
580 1
        if args.document:
581 1
            error("argument -d/--document: not allowed with 'all'")
582
583
    # Build tree
584 1
    item = None
585 1
    document = None
586 1
    tree = tree = _get_tree(args, cwd)
587
588
    # Determine if tree, document, or item was requested
589 1
    if args.label != 'all':
590 1
        if not args.item:
591 1
            try:
592 1
                document = tree.find_document(args.label)
593 1
            except common.DoorstopError as exc:
594 1
                if args.document:
595 1
                    raise exc from None
596 1
        if not document:
597 1
            item = tree.find_item(args.label)
598
599
    # Yield items from the requested object
600 1
    if item:
601 1
        yield item
602 1
    elif document:
603 1
        for item in document:
604 1
            yield item
605
    else:
606 1
        for document in tree:
607 1
            for item in document:
608 1
                yield item
609
610
611 1
def _export_import(args, cwd, error, document, ext):
612
    """Edit a document by calling export followed by import.
613
614
    :param args: Namespace of CLI arguments
615
    :param cwd: current working directory
616
    :param error: function to call for CLI errors
617
    :param document: :class:`~doorstop.core.document.Document` to edit
618
    :param ext: extension for export format
619
620
    """
621
    # Export the document to file
622 1
    args.prefix = document.prefix
623 1
    path = "{}-{}{}".format(args.prefix, int(time.time()), ext)
624 1
    args.path = path
625 1
    get('export')(args, cwd, error, catch=False, auto=True,
626
                  _tree=document.tree)
627
628
    # Open the exported file
629 1
    editor.edit(path, tool=args.tool)
630
631
    # Import the file to the same document
632 1
    if utilities.ask("import from '{}'?".format(path)):
633 1
        args.attrs = {}
634 1
        args.map = {}
635 1
        get('import')(args, cwd, error, catch=False, _tree=document.tree)
636 1
        common.delete(path)
637
    else:
638 1
        utilities.show("import canceled")
639 1
        if utilities.ask("delete '{}'?".format(path)):
640 1
            common.delete(path)
641
        else:
642 1
            msg = "to manually import: doorstop import {0}".format(path)
643
            utilities.show(msg)
644