Completed
Push — develop ( 9665ab...5977d5 )
by Jace
22s queued 11s
created

doorstop.cli.main._edit()   B

Complexity

Conditions 1

Size

Total Lines 50
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 1

Importance

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