doorstop.cli.utilities   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 267
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 64
eloc 165
dl 0
loc 267
ccs 150
cts 150
cp 1
rs 3.28
c 0
b 0
f 0

7 Functions

Rating   Name   Duplication   Size   Complexity  
B configure_logging() 0 43 8
A positive_int() 0 17 4
F configure_settings() 0 46 22
C get_ext() 0 50 11
A show() 0 12 4
A literal_eval() 0 23 4
A ask() 0 26 4

4 Methods

Rating   Name   Duplication   Size   Complexity  
A capture.__enter__() 0 2 1
A capture.__exit__() 0 7 4
A capture.__init__() 0 3 1
A capture.__bool__() 0 2 1

How to fix   Complexity   

Complexity

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