doorstop.cli.utilities.ask()   A
last analyzed

Complexity

Conditions 4

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 26
rs 9.6
c 0
b 0
f 0
cc 4
nop 2
1
# SPDX-License-Identifier: LGPL-3.0-only
2
3
"""Shared functions for the `doorstop.cli` package."""
4
5
import ast
6
import logging
7
import os
8
from argparse import ArgumentTypeError
9
10
from doorstop import common, settings
11
12
log = common.logger(__name__)
13
14
15
class capture:  # pylint: disable=R0903
16
    """Context manager to catch :class:`~doorstop.common.DoorstopError`."""
17
18
    def __init__(self, catch=True):
19
        self.catch = catch
20
        self._success = True
21
22
    def __bool__(self):
23
        return self._success
24
25
    def __enter__(self):
26
        return self
27
28
    def __exit__(self, exc_type, exc_value, traceback):
29
        if exc_type and issubclass(exc_type, common.DoorstopError):
30
            self._success = False
31
            if self.catch:
32
                log.error(exc_value)
33
                return True
34
        return False
35
36
37
def configure_logging(verbosity=0):
38
    """Configure logging using the provided verbosity level (0+)."""
39
    assert common.PRINT_VERBOSITY == 0
40
    assert common.STR_VERBOSITY == 3
41
    assert common.MAX_VERBOSITY == 4
42
43
    # Configure the logging level and format
44
    if verbosity == -1:
45
        level = settings.QUIET_LOGGING_LEVEL
46
        default_format = settings.DEFAULT_LOGGING_FORMAT
47
        verbose_format = settings.LEVELED_LOGGING_FORMAT
48
    elif verbosity == 0:
49
        level = settings.DEFAULT_LOGGING_LEVEL
50
        default_format = settings.DEFAULT_LOGGING_FORMAT
51
        verbose_format = settings.LEVELED_LOGGING_FORMAT
52
    elif verbosity == 1:
53
        level = settings.VERBOSE_LOGGING_LEVEL
54
        default_format = settings.DEFAULT_LOGGING_FORMAT
55
        verbose_format = settings.LEVELED_LOGGING_FORMAT
56
    elif verbosity == 2:
57
        level = settings.VERBOSE2_LOGGING_LEVEL
58
        default_format = verbose_format = settings.VERBOSE_LOGGING_FORMAT
59
    elif verbosity == 3:
60
        level = settings.VERBOSE3_LOGGING_LEVEL
61
        default_format = verbose_format = settings.VERBOSE_LOGGING_FORMAT
62
    else:
63
        level = settings.VERBOSE3_LOGGING_LEVEL
64
        default_format = verbose_format = settings.VERBOSE2_LOGGING_FORMAT
65
66
    # Set a custom formatter
67
    if not logging.root.handlers:
68
        logging.basicConfig(level=level)
69
        logging.captureWarnings(True)
70
        formatter = common.WarningFormatter(default_format, verbose_format)
71
        logging.root.handlers[0].setFormatter(formatter)
72
73
    # Warn about excessive verbosity
74
    if verbosity > common.MAX_VERBOSITY:
75
        msg = "maximum verbosity level is {}".format(common.MAX_VERBOSITY)
76
        logging.warning(msg)
77
        common.verbosity = common.MAX_VERBOSITY
78
    else:
79
        common.verbosity = verbosity
80
81
82
def configure_settings(args):
83
    """Update settings based on the command-line options."""
84
85
    # Parse common settings
86
    if args.no_reformat is not None:
87
        settings.REFORMAT = args.no_reformat is False
88
    if args.reorder is not None:
89
        settings.REORDER = args.reorder is True
90
    if args.no_level_check is not None:
91
        settings.CHECK_LEVELS = args.no_level_check is False
92
    if args.no_ref_check is not None:
93
        settings.CHECK_REF = args.no_ref_check is False
94
    if args.no_child_check is not None:
95
        settings.CHECK_CHILD_LINKS = args.no_child_check is False
96
    if args.strict_child_check is not None:
97
        settings.CHECK_CHILD_LINKS_STRICT = args.strict_child_check is True
98
    if args.no_suspect_check is not None:
99
        settings.CHECK_SUSPECT_LINKS = args.no_suspect_check is False
100
    if args.no_review_check is not None:
101
        settings.CHECK_REVIEW_STATUS = args.no_review_check is False
102
    if args.no_cache is not None:
103
        settings.CACHE_DOCUMENTS = args.no_cache is False
104
        settings.CACHE_ITEMS = args.no_cache is False
105
        settings.CACHE_PATHS = args.no_cache is False
106
    if args.warn_all is not None:
107
        settings.WARN_ALL = args.warn_all is True
108
    if args.error_all is not None:
109
        settings.ERROR_ALL = args.error_all is True
110
111
    # Parse `add` settings
112
    if hasattr(args, "server") and args.server is not None:
113
        settings.SERVER_HOST = args.server
114
    if hasattr(args, "port") and args.port is not None:
115
        settings.SERVER_PORT = args.port
116
117
    # Parse `publish` settings
118
    if hasattr(args, "no_child_links") and args.no_child_links is not None:
119
        settings.PUBLISH_CHILD_LINKS = args.no_child_links is False
120
    if hasattr(args, "no_levels") and args.no_levels is not None:
121
        settings.PUBLISH_BODY_LEVELS = False
122
        settings.PUBLISH_HEADING_LEVELS = args.no_levels != "all"
123
124
125
def literal_eval(literal, error=None, default=None):
126
    """Convert an literal to its value.
127
128
    :param literal: string to evaulate
129
    :param error: function to call for errors
130
    :param default: default value for empty inputs
131
    :return: Python literal
132
133
    >>> literal_eval("False")
134
    False
135
136
    >>> literal_eval("[1, 2, 3]")
137
    [1, 2, 3]
138
139
    """
140
    try:
141
        return ast.literal_eval(literal) if literal else default
142
    except (SyntaxError, ValueError):
143
        msg = "invalid Python literal: {}".format(literal)
144
        if error:
145
            error(msg)
146
        else:
147
            log.critical(msg)
148
        return None
149
150
151
def get_ext(args, error, ext_stdout, ext_file, whole_tree=False):
152
    """Determine the output file extensions from input arguments.
153
154
    :param args: Namespace of CLI arguments
155
    :param error: function to call for CLI errors
156
    :param ext_stdout: default extension for standard output
157
    :param ext_file: default extension for file output
158
    :param whole_tree: indicates the path is a directory for the whole tree
159
160
    :return: chosen extension
161
162
    """
163
    path = args.path if hasattr(args, "path") else None
164
    ext = None
165
166
    # Get the default argument from a provided output path
167
    if path:
168
        if whole_tree:
169
            ext = ext_file
170
        else:
171
            if os.path.isdir(path):
172
                error("given a prefix, [path] must be a file, not a directory")
173
            ext = os.path.splitext(path)[-1]
174
        log.debug("extension based on path: {}".format(ext or None))
175
176
    # Override the extension if a format is specified
177
    for _ext, option in {
178
        ".txt": "text",
179
        ".md": "markdown",
180
        ".html": "html",
181
        ".yml": "yaml",
182
        ".csv": "csv",
183
        ".xlsx": "xlsx",
184
        ".tex": "latex",
185
    }.items():
186
        try:
187
            if getattr(args, option):
188
                ext = _ext
189
                log.debug("extension based on override: {}".format(ext))
190
                break
191
        except AttributeError:
192
            continue
193
    else:
194
        if not ext:
195
            if path:
196
                error("given a prefix, [path] must include an extension")
197
            else:
198
                ext = ext_stdout
199
            log.debug("extension based on default: {}".format(ext))
200
201
    return ext
202
203
204
def show(message, flush=False):
205
    """Print (optionally flushed) text to the display.
206
207
    :param message: text to print
208
    :param flush: indicates the message is progress text
209
210
    """
211
    # show messages when enabled
212
    if common.verbosity >= common.PRINT_VERBOSITY:
213
        # unless they are progress messages and logging is enabled
214
        if common.verbosity == 0 or not flush:
215
            print(message, flush=flush)
216
217
218
def ask(question, default=None):
219
    """Display a console yes/no prompt.
220
221
    :param question: text of yes/no question ending in '?'
222
    :param default: 'yes', 'no', or None (for no default)
223
224
    :return: True = 'yes', False = 'no'
225
226
    """
227
    valid = {"yes": True, "y": True, "no": False, "n": False}
228
    prompts = {"yes": " [Y/n] ", "no": " [y/N] ", None: " [y/n] "}
229
230
    prompt = prompts.get(default, prompts[None])
231
    message = question + prompt
232
233
    while True:
234
        try:
235
            choice = input(message).lower().strip() or default
236
        except KeyboardInterrupt as exc:
237
            print()
238
            raise exc from None  # pylint: disable=raising-bad-type
239
        try:
240
            return valid[choice]
241
        except KeyError:
242
            options = ", ".join(sorted(valid.keys()))
243
            print("valid responses: {}".format(options))
244
245
246
def positive_int(value):
247
    """Evaluate a value as positive.
248
249
    :param value: passed in value to Evaluate
250
251
    :return: value casted to an integer
252
253
    """
254
    exc = ArgumentTypeError("'{}' is not a positive int value".format(value))
255
    try:
256
        ival = int(value)
257
    except ValueError:
258
        raise exc from None  # pylint: disable=raising-bad-type
259
    else:
260
        if ival < 1:
261
            raise exc
262
        return ival
263