Passed
Push — develop ( dd68d8...050420 )
by
unknown
02:47 queued 18s
created

doorstop.cli.main._add()   B

Complexity

Conditions 1

Size

Total Lines 57
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 40
dl 0
loc 57
rs 8.92
c 0
b 0
f 0
cc 1
nop 2

How to fix   Long Method   

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:

1
#!/usr/bin/env python
2
# SPDX-License-Identifier: LGPL-3.0-only
3
# pylint: disable=import-outside-toplevel
4
5
"""Command-line interface for Doorstop."""
6
7
import argparse
8
import os
9
import sys
10
11
from doorstop import common, settings
12
from doorstop.cli import commands, utilities
13
from doorstop.core import document, vcs
14
15
log = common.logger(__name__)
16
17
EDITOR = os.environ.get("EDITOR")
18
19
20
def main(args=None):  # pylint: disable=R0915
21
    """Process command-line arguments and run the program."""
22
    from doorstop import CLI, DESCRIPTION, VERSION
23
24
    # Shared options
25
    project = argparse.ArgumentParser(add_help=False)
26
    try:
27
        root = vcs.find_root(os.getcwd())
28
    except common.DoorstopError:
29
        root = None
30
    project.add_argument(
31
        "-j",
32
        "--project",
33
        metavar="PATH",
34
        help="path to the root of the project",
35
        default=root,
36
    )
37
    project.add_argument("--no-cache", action="store_true", help=argparse.SUPPRESS)
38
    server = argparse.ArgumentParser(add_help=False)
39
    server.add_argument(
40
        "--server",
41
        metavar="HOST",
42
        help="IP address or hostname for a running server",
43
        default=settings.SERVER_HOST,
44
    )
45
    server.add_argument(
46
        "--port",
47
        metavar="NUMBER",
48
        type=int,
49
        help="use a custom port for the server",
50
        default=settings.SERVER_PORT,
51
    )
52
    server.add_argument(
53
        "-f",
54
        "--force",
55
        action="store_true",
56
        help="perform the action without the server",
57
    )
58
    debug = argparse.ArgumentParser(add_help=False)
59
    debug.add_argument("-V", "--version", action="version", version=VERSION)
60
    group = debug.add_mutually_exclusive_group()
61
    group.add_argument(
62
        "-v", "--verbose", action="count", default=0, help="enable verbose logging"
63
    )
64
    group.add_argument(
65
        "-q",
66
        "--quiet",
67
        action="store_const",
68
        const=-1,
69
        dest="verbose",
70
        help="only display errors and prompts",
71
    )
72
    shared = {
73
        "formatter_class": common.HelpFormatter,
74
        "parents": [project, server, debug],
75
    }
76
77
    # Build main parser
78
    parser = argparse.ArgumentParser(
79
        prog=CLI, description=DESCRIPTION, **shared  # type: ignore
80
    )
81
    parser.add_argument(
82
        "-F",
83
        "--no-reformat",
84
        action="store_true",
85
        help="do not reformat item files during validation",
86
    )
87
    parser.add_argument(
88
        "-r",
89
        "--reorder",
90
        action="store_true",
91
        help="reorder document levels during validation",
92
    )
93
    parser.add_argument(
94
        "-L",
95
        "--no-level-check",
96
        action="store_true",
97
        help="do not validate document levels",
98
    )
99
    parser.add_argument(
100
        "-R",
101
        "--no-ref-check",
102
        action="store_true",
103
        help="do not validate external file references",
104
    )
105
    parser.add_argument(
106
        "-C",
107
        "--no-child-check",
108
        action="store_true",
109
        help="do not validate child (reverse) links",
110
    )
111
    parser.add_argument(
112
        "-Z",
113
        "--strict-child-check",
114
        action="store_true",
115
        help="require child (reverse) links from every document",
116
    )
117
    parser.add_argument(
118
        "-S",
119
        "--no-suspect-check",
120
        action="store_true",
121
        help="do not check for suspect links",
122
    )
123
    parser.add_argument(
124
        "-W",
125
        "--no-review-check",
126
        action="store_true",
127
        help="do not check item review status",
128
    )
129
    parser.add_argument(
130
        "-s",
131
        "--skip",
132
        metavar="PREFIX",
133
        action="append",
134
        help="skip a document during validation",
135
    )
136
    parser.add_argument(
137
        "-w",
138
        "--warn-all",
139
        action="store_true",
140
        help="display all info-level issues as warnings",
141
    )
142
    parser.add_argument(
143
        "-e",
144
        "--error-all",
145
        action="store_true",
146
        help="display all warning-level issues as errors",
147
    )
148
149
    # Build sub-parsers
150
    subs = parser.add_subparsers(help="", dest="command", metavar="<command>")
151
    _create(subs, shared)
152
    _delete(subs, shared)
153
    _add(subs, shared)
154
    _remove(subs, shared)
155
    _edit(subs, shared)
156
    _reorder(subs, shared)
157
    _link(subs, shared)
158
    _unlink(subs, shared)
159
    _clear(subs, shared)
160
    _review(subs, shared)
161
    _import(subs, shared)
162
    _export(subs, shared)
163
    _publish(subs, shared)
164
165
    # Parse arguments
166
    args = parser.parse_args(args=args)
167
168
    # Configure logging
169
    utilities.configure_logging(args.verbose)
170
171
    # Configure settings
172
    utilities.configure_settings(args)
173
174
    # Run the program
175
    function = commands.get(args.command)
176
    try:
177
        success = function(args, os.getcwd(), parser.error)
178
    except common.DoorstopFileError as exc:
179
        log.error(exc)
180
        success = False
181
    except KeyboardInterrupt:
182
        log.debug("command cancelled")
183
        success = False
184
    if success:
185
        log.debug("command succeeded")
186
    else:
187
        log.debug("command failed")
188
        sys.exit(1)
189
190
191
def _create(subs, shared):
192
    """Configure the `doorstop create` subparser."""
193
    info = "create a new document directory"
194
    sub = subs.add_parser(
195
        "create", description=info.capitalize() + ".", help=info, **shared
196
    )
197
    sub.add_argument("prefix", help="document prefix for new item UIDs")
198
    sub.add_argument("path", help="path to a directory for item files")
199
    sub.add_argument("-p", "--parent", help="prefix of parent document")
200
    sub.add_argument(
201
        "-i",
202
        "--itemformat",
203
        choices=["yaml", "markdown"],
204
        default="yaml",
205
        help="item file format",
206
    )
207
    sub.add_argument(
208
        "-d",
209
        "--digits",
210
        help="number of digits in item UIDs",
211
        default=document.Document.DEFAULT_DIGITS,
212
    )
213
    sub.add_argument(
214
        "-s",
215
        "--separator",
216
        metavar="SEP",
217
        help=(
218
            "separator between the prefix and the number or name in an "
219
            "item UID; the only valid separators are '-', '_', and '.'"
220
        ),
221
        default=document.Document.DEFAULT_SEP,
222
    )
223
224
225
def _delete(subs, shared):
226
    """Configure the `doorstop delete` subparser."""
227
    info = "delete a document directory"
228
    sub = subs.add_parser(
229
        "delete", description=info.capitalize() + ".", help=info, **shared
230
    )
231
    sub.add_argument("prefix", help="prefix of document to delete")
232
233
234
def _add(subs, shared):
235
    """Configure the `doorstop add` subparser."""
236
    info = "create an item file in a document directory"
237
    sub = subs.add_parser(
238
        "add", description=info.capitalize() + ".", help=info, **shared
239
    )
240
    sub.add_argument("prefix", help="document prefix for the new item")
241
    sub.add_argument("-l", "--level", help="desired item level (e.g. 1.2.3)")
242
    sub.add_argument(
243
        "-n",
244
        "--name",
245
        "--number",
246
        metavar="NANU",
247
        help=(
248
            "use the specified name or number NANU instead of an automatically "
249
            "generated number for the UID (together with the document prefix "
250
            "and separator); the NANU must be a number or a string which does "
251
            "not contain separator characters"
252
        ),
253
    )
254
    sub.add_argument(
255
        "-c",
256
        "--count",
257
        default=1,
258
        type=utilities.positive_int,
259
        help="number of items to create",
260
    )
261
    sub.add_argument(
262
        "--edit",
263
        action="store_true",
264
        help=(
265
            "Open default editor to edit the added item. "
266
            "Default editor can be set using the environment "
267
            "variable EDITOR."
268
        ),
269
    )
270
    sub.add_argument(
271
        "-T",
272
        "--tool",
273
        metavar="PROGRAM",
274
        default=EDITOR,
275
        help=(
276
            "text editor to open the document item (only"
277
            "required if $EDITOR is not found in"
278
            "environment). Useless option without --edit"
279
        ),
280
    )
281
    sub.add_argument(
282
        "-d",
283
        "--defaults",
284
        metavar="FILE",
285
        help=("file in YAML format with default values for attributes of the new item"),
286
    )
287
    sub.add_argument(
288
        "--noreorder",
289
        action="store_false",
290
        help=("disable automatic reordering of file"),
291
    )
292
293
294
def _remove(subs, shared):
295
    """Configure the `doorstop remove` subparser."""
296
    info = "remove an item file from a document directory"
297
    sub = subs.add_parser(
298
        "remove", description=info.capitalize() + ".", help=info, **shared
299
    )
300
    sub.add_argument("uid", help="item UID to remove from its document")
301
302
303
def _edit(subs, shared):
304
    """Configure the `doorstop edit` subparser."""
305
    info = "open an existing item or document for editing"
306
    sub = subs.add_parser(
307
        "edit", description=info.capitalize() + ".", help=info, **shared
308
    )
309
    sub.add_argument("label", help="item UID or document prefix to open for editing")
310
    sub.add_argument(
311
        "-a",
312
        "--all",
313
        action="store_true",
314
        help=(
315
            "Edit the whole item with all its attributes. "
316
            "Without this option, only its text is opened for "
317
            "edition. Useless when editing a whole document."
318
        ),
319
    )
320
    group = sub.add_mutually_exclusive_group()
321
    group.add_argument(
322
        "-i", "--item", action="store_true", help="indicates the 'label' is an item UID"
323
    )
324
    group.add_argument(
325
        "-d",
326
        "--document",
327
        action="store_true",
328
        help="indicates the 'label' is a document prefix",
329
    )
330
    group = sub.add_mutually_exclusive_group()
331
    group.add_argument(
332
        "-y",
333
        "--yaml",
334
        action="store_true",
335
        help="edit document as exported YAML (default)",
336
    )
337
    group.add_argument(
338
        "-c", "--csv", action="store_true", help="edit document as exported CSV"
339
    )
340
    group.add_argument(
341
        "-t", "--tsv", action="store_true", help="edit document as exported TSV"
342
    )
343
    group.add_argument(
344
        "-x", "--xlsx", action="store_true", help="edit document as exported XLSX"
345
    )
346
    required = sub.add_argument_group("required arguments")
347
    required.add_argument(
348
        "-T",
349
        "--tool",
350
        metavar="PROGRAM",
351
        default=EDITOR,
352
        help="text editor to open the document item (only required if $EDITOR is not found in environment)",
353
    )
354
355
356
def _reorder(subs, shared):
357
    """Configure the `doorstop reorder` subparser."""
358
    info = "organize the outline structure of a document"
359
    sub = subs.add_parser(
360
        "reorder", description=info.capitalize() + ".", help=info, **shared
361
    )
362
    sub.add_argument("prefix", help="prefix of document to reorder")
363
    group = sub.add_mutually_exclusive_group()
364
    group.add_argument(
365
        "-a",
366
        "--auto",
367
        action="store_true",
368
        help="only perform automatic item reordering",
369
    )
370
    group.add_argument(
371
        "-m",
372
        "--manual",
373
        action="store_true",
374
        help="do not automatically reorder the items",
375
    )
376
    sub.add_argument(
377
        "-T",
378
        "--tool",
379
        metavar="PROGRAM",
380
        default=EDITOR,
381
        help="text editor to open the document index",
382
    )
383
384
385
def _link(subs, shared):
386
    """Configure the `doorstop link` subparser."""
387
    info = "add a new link between two items"
388
    sub = subs.add_parser(
389
        "link", description=info.capitalize() + ".", help=info, **shared
390
    )
391
    sub.add_argument("child", help="child item UID to link to the parent")
392
    sub.add_argument("parent", help="parent item UID to link from the child")
393
394
395
def _unlink(subs, shared):
396
    """Configure the `doorstop unlink` subparser."""
397
    info = "remove a link between two items"
398
    sub = subs.add_parser(
399
        "unlink", description=info.capitalize() + ".", help=info, **shared
400
    )
401
    sub.add_argument("child", help="child item UID to unlink from parent")
402
    sub.add_argument("parent", help="parent item UID child is linked to")
403
404
405
def _clear(subs, shared):
406
    """Configure the `doorstop clear` subparser."""
407
    info = "absolve items of their suspect link status"
408
    sub = subs.add_parser(
409
        "clear", description=info.capitalize() + ".", help=info, **shared
410
    )
411
    sub.add_argument("label", help="item UID, document prefix, or 'all'")
412
    group = sub.add_mutually_exclusive_group()
413
    group.add_argument(
414
        "-i", "--item", action="store_true", help="indicates the 'label' is an item UID"
415
    )
416
    group.add_argument(
417
        "-d",
418
        "--document",
419
        action="store_true",
420
        help="indicates the 'label' is a document prefix",
421
    )
422
    sub.add_argument(
423
        "parents", nargs="*", help="only clear links with these parent item UIDs"
424
    )
425
426
427
def _review(subs, shared):
428
    """Configure the `doorstop review` subparser."""
429
    info = "absolve items of their unreviewed status"
430
    sub = subs.add_parser(
431
        "review", description=info.capitalize() + ".", help=info, **shared
432
    )
433
    sub.add_argument("label", help="item UID, document prefix, or 'all'")
434
    group = sub.add_mutually_exclusive_group()
435
    group.add_argument(
436
        "-i", "--item", action="store_true", help="indicates the 'label' is an item UID"
437
    )
438
    group.add_argument(
439
        "-d",
440
        "--document",
441
        action="store_true",
442
        help="indicates the 'label' is a document prefix",
443
    )
444
445
446 View Code Duplication
def _import(subs, shared):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
447
    """Configure the `doorstop import` subparser."""
448
    info = "import an existing document or item"
449
    sub = subs.add_parser(
450
        "import", description=info.capitalize() + ".", help=info, **shared
451
    )
452
    sub.add_argument(
453
        "path", nargs="?", help="path to previously exported document file"
454
    )
455
    sub.add_argument("prefix", nargs="?", help="prefix of document for import")
456
    group = sub.add_mutually_exclusive_group()
457
    group.add_argument(
458
        "-d",
459
        "--document",
460
        nargs=2,
461
        metavar="ARG",
462
        help="import an existing document by: PREFIX PATH",
463
    )
464
    group.add_argument(
465
        "-i",
466
        "--item",
467
        nargs=2,
468
        metavar="ARG",
469
        help="import an existing item by: PREFIX UID",
470
    )
471
    sub.add_argument(
472
        "-p",
473
        "--parent",
474
        metavar="PREFIX",
475
        help="parent document prefix for imported document",
476
    )
477
    sub.add_argument(
478
        "-a", "--attrs", metavar="DICT", help="dictionary of item attributes to import"
479
    )
480
    sub.add_argument(
481
        "-m", "--map", metavar="DICT", help="dictionary of custom item attribute names"
482
    )
483
484
485 View Code Duplication
def _export(subs, shared):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
486
    """Configure the `doorstop export` subparser."""
487
    info = "export a document as YAML or another format"
488
    sub = subs.add_parser(
489
        "export", description=info.capitalize() + ".", help=info, **shared
490
    )
491
    sub.add_argument("prefix", help="prefix of document to export or 'all'")
492
    sub.add_argument(
493
        "path", nargs="?", help="path to exported file or directory for 'all'"
494
    )
495
    group = sub.add_mutually_exclusive_group()
496
    group.add_argument(
497
        "-y", "--yaml", action="store_true", help="output YAML (default when no path)"
498
    )
499
    group.add_argument(
500
        "-c", "--csv", action="store_true", help="output CSV (default for 'all')"
501
    )
502
    group.add_argument("-t", "--tsv", action="store_true", help="output TSV")
503
    group.add_argument("-x", "--xlsx", action="store_true", help="output XLSX")
504
    sub.add_argument("-w", "--width", type=int, help="limit line width on text output")
505
506
507
def _publish(subs, shared):
508
    """Configure the `doorstop publish` subparser."""
509
    info = "publish a document as text or another format"
510
    sub = subs.add_parser(
511
        "publish", description=info.capitalize() + ".", help=info, **shared
512
    )
513
    sub.add_argument("prefix", help="prefix of document to publish or 'all'")
514
    sub.add_argument(
515
        "path", nargs="?", help="path to published file or directory for 'all'"
516
    )
517
    group = sub.add_mutually_exclusive_group()
518
    group.add_argument(
519
        "-t", "--text", action="store_true", help="output text (default when no path)"
520
    )
521
    group.add_argument("-m", "--markdown", action="store_true", help="output Markdown")
522
    group.add_argument("-l", "--latex", action="store_true", help="output LaTeX")
523
    group.add_argument(
524
        "-H", "--html", action="store_true", help="output HTML (default for 'all')"
525
    )
526
    sub.add_argument("-w", "--width", type=int, help="limit line width on text output")
527
    sub.add_argument(
528
        "-C",
529
        "--no-child-links",
530
        action="store_true",
531
        help="do not include child links on items",
532
    )
533
    sub.add_argument(
534
        "--no-levels",
535
        choices=["all", "body"],
536
        help="do not include levels on heading and non-heading or non-heading items",
537
    )
538
    sub.add_argument("--template", help="template file", default=None)
539
540
541
if __name__ == "__main__":
542
    main()
543