File: application.py

package info (click to toggle)
crazy-complete 0.3.6-2
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 2,404 kB
  • sloc: python: 7,949; sh: 4,636; makefile: 74
file content (264 lines) | stat: -rw-r--r-- 9,366 bytes parent folder | download | duplicates (3)
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
'''Class for main command line application.'''

import os
import argparse

from .errors import CrazyError
from . import bash, fish, zsh
from . import argparse_source, json_source, yaml_source
from . import argparse_mod # .complete()
from . import help_converter
from . import utils
from . import config
from . import paths

# The import of `argparse_mod` only modifies the classes provided by the argparse
# module. We use the dummy function to silence warnings about the unused import.
argparse_mod.dummy()


def boolean(string):
    '''Convert string to bool, else raise ValueError.'''

    try:
        return {'true': True, 'false': False}[string.lower()]
    except KeyError as e:
        raise ValueError(f"Not a bool: {string}") from e


def feature_list(string):
    '''Convert a comma separated string of features to list.'''

    features = string.split(',')

    for feature in features:
        if feature not in ('hidden', 'final', 'groups', 'repeatable', 'when'):
            raise ValueError(f"Invalid feature: {feature}")

    return features


p = argparse.ArgumentParser('crazy-complete',
    description='Generate shell auto completion files for all major shells',
    exit_on_error=False)

p.add_argument('shell', choices=('bash', 'fish', 'zsh', 'json', 'yaml'),
    help='Specify the shell type for the completion script')

p.add_argument('definition_file',
    help='The file containing the command line definitions'
).complete('file')

p.add_argument('--version', action='version', version='%(prog)s 0.3.6',
    help='Show program version')

p.add_argument('--parser-variable', default=None,
    help='Specify the variable name of the ArgumentParser object (for --input-type=python)')

p.add_argument('--input-type', choices=('yaml', 'json', 'python', 'help', 'auto'), default='auto',
    help='Specify input file type')

p.add_argument('--abbreviate-commands', metavar='BOOL', default=False, type=boolean,
    help='Sets whether commands can be abbreviated'
).complete('choices', ('True', 'False'))

p.add_argument('--abbreviate-options', metavar='BOOL', default=False, type=boolean,
    help='Sets whether options can be abbreviated'
).complete('choices', ('True', 'False'))

p.add_argument('--repeatable-options', metavar='BOOL', default=False, type=boolean,
    help='Sets whether options are suggested multiple times during completion'
).complete('choices', ('True', 'False'))

p.add_argument('--inherit-options', metavar='BOOL', default=False, type=boolean,
    help='Sets whether parent options are visible to subcommands'
).complete('choices', ('True', 'False'))

p.add_argument('--disable', metavar='FEATURES', default=[], type=feature_list,
    help='Disable features (hidden,final,groups,repeatable,when)'
).complete('value_list', {'values': {
    'hidden':     'Disable hidden options',
    'final':      'Disable final options',
    'groups':     'Disable option groups',
    'repeatable': 'Disable check for repeatable options',
    'when':       'Disable conditional options and positionals',
}})

p.add_argument('--vim-modeline', metavar='BOOL', default=True, type=boolean,
    help='Sets whether a vim modeline comment shall be appended to the generated code'
).complete('choices', ('True', 'False'))

p.add_argument('--zsh-compdef', metavar='BOOL', default=True, type=boolean,
    help='Sets whether #compdef is used in zsh scripts'
).complete('choices', ('True', 'False'))

p.add_argument('--fish-fast', metavar='BOOL', default=False, type=boolean,
    help='Use faster commandline parsing at the cost of correctness'
).complete('choices', ('True', 'False'))

p.add_argument('--fish-inline-conditions', metavar='BOOL', default=False, type=boolean,
    help="Don't store conditions in a variable"
).complete('choices', ('True', 'False'))

p.add_argument('--include-file', metavar='FILE', action='append',
    help='Include file in output'
).complete('file')

p.add_argument('--debug', action='store_true', default=False,
    help='Enable debug mode')

grp = p.add_mutually_exclusive_group()

grp.add_argument('-o', '--output', metavar='FILE', default=None, dest='output_file',
    help='Write output to destination file [default: stdout]'
).complete('file')

grp.add_argument('-i', '--install-system-wide', default=False, action='store_true',
    help='Write output to the system-wide completions dir of shell')

grp.add_argument('-u', '--uninstall-system-wide', default=False, action='store_true',
    help='Uninstall the system-wide completion file for program')

# We use a unique object name for avoiding name clashes when
# importing/executing the foreign python script
_crazy_complete_argument_parser = p
del p


def write_string_to_file(string, file):
    '''Writes string to file if file is given.'''

    if file is not None:
        with open(file, 'w', encoding='utf-8') as fh:
            fh.write(string)
    else:
        print(string)


def load_definition_file(opts):
    '''Load a definition file as specified in `opts`.'''

    if opts.input_type == 'auto':
        basename = os.path.basename(opts.definition_file)
        extension = os.path.splitext(basename)[1].lower().strip('.')
        if extension == 'json':
            return json_source.load_from_file(opts.definition_file)
        if extension in ('yaml', 'yml'):
            return yaml_source.load_from_file(opts.definition_file)
        if extension == 'py':
            raise CrazyError('Reading Python files must be enabled by --input-type=python')
        if extension == '':
            raise CrazyError('File has no extension. Please supply --input-type=json|yaml|help|python')
        raise CrazyError(f'Unknown file extension `{extension}`. Please supply --input-type=json|yaml|help|python')
    if opts.input_type == 'json':
        return json_source.load_from_file(opts.definition_file)
    if opts.input_type == 'yaml':
        return yaml_source.load_from_file(opts.definition_file)
    if opts.input_type == 'python':
        return argparse_source.load_from_file(opts.definition_file,
            opts.parser_variable,
            parser_blacklist=[_crazy_complete_argument_parser])

    raise AssertionError("Should not be reached")


def _get_config_from_options(opts):
    conf = config.Config()
    conf.set_abbreviate_commands(opts.abbreviate_commands)
    conf.set_abbreviate_options(opts.abbreviate_options)
    conf.set_repeatable_options(opts.repeatable_options)
    conf.set_inherit_options(opts.inherit_options)
    conf.set_vim_modeline(opts.vim_modeline)
    conf.set_zsh_compdef(opts.zsh_compdef)
    conf.set_fish_fast(opts.fish_fast)
    conf.set_fish_inline_conditions(opts.fish_inline_conditions)
    conf.include_many_files(opts.include_file or [])

    for feature in opts.disable:
        if feature == 'hidden':
            conf.disable_hidden(True)
        elif feature == 'final':
            conf.disable_final(True)
        elif feature == 'groups':
            conf.disable_groups(True)
        elif feature == 'repeatable':
            conf.disable_repeatable(True)
        elif feature == 'when':
            conf.disable_when(True)

    return conf


def generate(opts):
    '''Generate output file as specified in `opts`.'''

    if opts.input_type == 'help':
        if opts.shell != 'yaml':
            raise CrazyError('The `help` input-type currently only supports YAML generation')
        output = help_converter.from_file_to_yaml(opts.definition_file)
        write_string_to_file(output, opts.output_file)
        return

    cmdline = load_definition_file(opts)

    if opts.shell == 'json':
        output = json_source.commandline_to_json(cmdline)
        write_string_to_file(output, opts.output_file)
        return

    if opts.shell == 'yaml':
        output = yaml_source.commandline_to_yaml(cmdline)
        write_string_to_file(output, opts.output_file)
        return

    conf = _get_config_from_options(opts)

    if opts.shell == 'bash':
        output = bash.generate_completion(cmdline, conf)
    elif opts.shell == 'fish':
        output = fish.generate_completion(cmdline, conf)
    elif opts.shell == 'zsh':
        output = zsh.generate_completion(cmdline, conf)

    if opts.install_system_wide or opts.uninstall_system_wide:
        file = {
            'bash':  paths.get_bash_completion_file,
            'fish':  paths.get_fish_completion_file,
            'zsh':   paths.get_zsh_completion_file,
        }[opts.shell](cmdline.prog)

        if opts.install_system_wide:
            utils.print_err(f'Installing to {file}')
            write_string_to_file(output, file)
        else:
            utils.print_err(f'Removing {file}')
            os.remove(file)
    else:
        write_string_to_file(output, opts.output_file)


class Application:
    '''Class for main command line application.'''

    def __init__(self):
        self.options = None

    def parse_args(self, args):
        '''Parse command line arguments.

        Raises:
            - argparse.ArgumentError
        '''
        self.options = _crazy_complete_argument_parser.parse_args(args)

    def run(self):
        '''Run the crazy-complete program.

        Raises:
            - CrazyError
            - FileNotFoundError
            - yaml.scanner.ScannerError
            - yaml.parser.ParserError
            - json.decoder.JSONDecodeError
        '''
        generate(self.options)