File: cli_utils.py

package info (click to toggle)
python-line-profiler 5.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,256 kB
  • sloc: python: 8,119; sh: 810; ansic: 297; makefile: 14
file content (298 lines) | stat: -rw-r--r-- 10,100 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
"""
Shared utilities between the :command:`python -m line_profiler` and
:command:`kernprof` CLI tools.
"""
import argparse
import functools
import os
import pathlib
import shutil
import sys
from .toml_config import ConfigSource


_BOOLEAN_VALUES = {**{k.casefold(): False
                      for k in ('', '0', 'off', 'False', 'F', 'no', 'N')},
                   **{k.casefold(): True
                      for k in ('1', 'on', 'True', 'T', 'yes', 'Y')}}


def add_argument(parser_like, arg, /, *args,
                 hide_complementary_options=True, **kwargs):
    """
    Override the ``'store_true'`` and ``'store_false'`` actions so that
    they are turned into options which:

    * Don't set the default to the opposite boolean, thus allowing us to
      later distinguish between cases where the flag has been passed or
      not, and
    * Set the destination value to the corresponding value in the no-arg
      form, but also allow (for long options) for a single arg which is
      parsed by :py:func:`.boolean()`.
    Also automatically generates complementary boolean options for
    ``action='store_true'`` options.
    If ``hide_complementary_options`` is
    true, the auto-generated option (all the long flags prefixed
    with ``'no-'``, e.g. ``'--foo'`` is negated by ``'--no-foo'``) is
    hidden from the help text.

    Arguments:
        parser_like (Any):
            Object having a method ``add_argument()``, which has the
            same semantics and call signature as
            :py:meth:`argparse.ArgumentParser.add_argument()`.
        hide_complementary_options (bool):
            Whether to hide the auto-generated complementary options to
            ``action='store_true'`` options from the help text for
            brevity.
        arg, *args, **kwargs
            Passed to ``parser_like.add_argument()``

    Returns:
        Any: action_like
            Return value of ``parser_like.add_argument()``

    Note:
        * Short and long flags for ``'store_true'`` and
          ``'store_false'`` actions are implemented in separate actions
          so as to allow for short-flag concatenation.
        * If an option has both short and long flags, the short-flag
          action is hidden from the help text, but the long-flag
          action's help text is updated to mention the corresponding
          short flag(s).
    """
    def negate_result(func):
        @functools.wraps(func)
        def negated(*args, **kwargs):
            return not func(*args, **kwargs)

        negated.__name__ = 'negated_' + negated.__name__
        return negated

    # Make sure there's at least one positional argument
    args = [arg, *args]

    if kwargs.get('action') not in ('store_true', 'store_false'):
        return parser_like.add_argument(*args, **kwargs)

    # Long and short boolean flags should be handled separately: short
    # flags should remain 0-arg to permit flag concatenation, while long
    # flag should be able to take an optional arg parsable into a bool
    prefix_chars = tuple(parser_like.prefix_chars)
    short_flags = []
    long_flags = []
    for arg in args:
        assert arg.startswith(prefix_chars)
        if arg.startswith(tuple(char * 2 for char in prefix_chars)):
            long_flags.append(arg)
        else:
            short_flags.append(arg)

    kwargs['const'] = const = kwargs.pop('action') == 'store_true'
    for key, value in dict(
            default=None,
            metavar='Y[es] | N[o] | T[rue] | F[alse] '
            '| on | off | 1 | 0').items():
        kwargs.setdefault(key, value)
    long_kwargs = kwargs.copy()
    short_kwargs = {**kwargs, 'action': 'store_const'}
    for key, value in dict(
            nargs='?',
            type=functools.partial(boolean, invert=not const)).items():
        long_kwargs.setdefault(key, value)

    # Mention the short options in the long options' documentation, and
    # suppress the short options in the help
    if (
            long_flags
            and short_flags
            and long_kwargs.get('help') != argparse.SUPPRESS):
        additional_msg = 'Short {}: {}'.format(
            'form' if len(short_flags) == 1 else 'forms',
            ', '.join(short_flags))
        if long_kwargs.get('help'):
            help_text = long_kwargs['help'].strip()
            if help_text.endswith((')', ']')):
                # Interpolate into existing parenthetical
                help_text = '{}; {}{}{}'.format(
                    help_text[:-1],
                    additional_msg[0].lower(),
                    additional_msg[1:],
                    help_text[-1])
            else:
                help_text = f'{help_text} ({additional_msg})'
            long_kwargs['help'] = help_text
        else:
            long_kwargs['help'] = f'({additional_msg})'
        short_kwargs['help'] = argparse.SUPPRESS

    long_action = short_action = None
    if long_flags:
        long_action = parser_like.add_argument(*long_flags, **long_kwargs)
        short_kwargs['dest'] = long_action.dest
    if short_flags:
        short_action = parser_like.add_argument(*short_flags, **short_kwargs)
    if long_action:
        action = long_action
    else:
        assert short_action
        action = short_action
    if not (const and long_flags):  # Negative or short-only flag
        return action

    # Automatically generate a complementary option for a long boolean
    # option
    # (in Python 3.9+ one can use `argparse.BooleanOptionalAction`,
    # but we want to maintain compatibility with Python 3.8)
    if hide_complementary_options:
        falsy_help_text = argparse.SUPPRESS
    else:
        falsy_help_text = 'Negate these flags: ' + ', '.join(args)
    parser_like.add_argument(
        *(flag[:2] + 'no-' + flag[2:] for flag in long_flags),
        **{**long_kwargs,
           'const': False,
           'dest': action.dest,
           'type': negate_result(action.type),
           'help': falsy_help_text})
    return action


def get_cli_config(subtable, /, *args, **kwargs):
    """
    Get the ``tool.line_profiler.<subtable>`` configs and normalize
    its keys (``some-key`` -> ``some_key``).

    Arguments:
        subtable (str):
            Name of the subtable the CLI app should refer to (e.g.
            ``'kernprof'``)
        *args, **kwargs
            Passed to \
:py:meth:`line_profiler.toml_config.ConfigSource.from_config`

    Returns:
        New :py:class:`~.line_profiler.toml_config.ConfigSource`
        instance
    """
    config = ConfigSource.from_config(*args, **kwargs).get_subconfig(subtable)
    config.conf_dict = {key.replace('-', '_'): value
                        for key, value in config.conf_dict.items()}
    return config


def get_python_executable():
    """
    Returns:
        str: command
            Command or path thereto corresponding to
            :py:data:`sys.executable`.
    """
    if os.path.samefile(shutil.which('python'), sys.executable):
        return 'python'
    elif os.path.samefile(shutil.which('python3'), sys.executable):
        return 'python3'
    else:
        return short_string_path(sys.executable)


def positive_float(value):
    """
    Arguments:
        value (str)

    Returns:
        float: positive_num
    """
    val = float(value)
    if val <= 0:
        # Note: parsing functions should raise either a `ValueError` or
        # a `TypeError` instead of an `argparse.ArgumentError`, which
        # expects extra context and in general should be raised by the
        # parser object
        raise ValueError
    return val


def boolean(value, *, fallback=None, invert=False):
    """
    Arguments:
        value (str)
            Value to be parsed into a boolean (case insensitive)
        fallback (Union[bool, None])
            Optional value to fall back to in case ``value`` doesn't
            match any of the specified
        invert (bool)
            If :py:data:`True`, invert the result of parsing ``value``
            (but not ``fallback``)

    Returns:
        bool: result

    Example:
        These values are parsed into :py:data:`False`:

        >>> assert not any(
        ...     boolean(value)
        ...     for value in ['', '0', 'F', 'N', 'off', 'False', 'no'])

        These values are parsed into :py:data:`True`:

        >>> assert all(
        ...     boolean(value)
        ...     for value in ['1', 'T', 'Y', 'on', 'True', 'yes'])

        Fallback:

        >>> assert boolean('invalid', fallback=True) == True
        >>> assert boolean('invalid', fallback=False) == False
        >>> try:
        ...     result = boolean('invalid')
        ... except ValueError:
        ...     pass
        ... except Exception as e:
        ...     assert False, (
        ...         f'Expected `ValueError`, got `{type(e).__name__}`')
        ... else:
        ...     assert False, (
        ...         f'Expected `ValueError`, got result {result!r}')

        Case insensitivity:

        >>> assert boolean('fAlSe') == False
        >>> assert boolean('YeS') == True
    """
    try:
        result = _BOOLEAN_VALUES[value.casefold()]
    except KeyError:
        pass
    else:
        return (not result) if invert else result
    if fallback is None:
        raise ValueError(f'value = {value!r}: '
                         'cannot be parsed into a boolean; valid values are'
                         f'({{string: bool}}): {_BOOLEAN_VALUES!r}')
    return fallback


def short_string_path(path):
    """
    Arguments:
        path (Union[str, PurePath]):
            Path-like

    Returns:
        str: short_path
            The shortest formatted ``path`` among the provided path, the
            corresponding absolute path, and its relative path to the
            current directory.
    """
    path = pathlib.Path(path)
    paths = {str(path)}
    abspath = path.absolute()
    paths.add(str(abspath))
    try:
        paths.add(str(abspath.relative_to(path.cwd().absolute())))
    except ValueError:  # Not relative to the curdir
        pass
    return min(paths, key=len)