Failed Conditions
Pull Request — master (#1990)
by Mischa
01:44
created

LinterBase   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 390
Duplicated Lines 0 %
Metric Value
dl 0
loc 390
rs 8.439
wmc 47

How to fix   Complexity   

Complex Class

Complex classes like LinterBase 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
from contextlib import contextmanager
2
import inspect
3
from itertools import compress
4
import re
5
import shutil
6
from subprocess import check_call, CalledProcessError, DEVNULL
7
from types import MappingProxyType
8
9
from coalib.bears.LocalBear import LocalBear
10
from coalib.misc.ContextManagers import make_temp
11
from coalib.misc.Decorators import assert_right_type, enforce_signature
12
from coalib.misc.Future import partialmethod
13
from coalib.misc.Shell import run_shell_command
14
from coalib.results.Diff import Diff
15
from coalib.results.Result import Result
16
from coalib.results.RESULT_SEVERITY import RESULT_SEVERITY
17
from coalib.settings.FunctionMetadata import FunctionMetadata
18
19
20
@enforce_signature
21
def Linter(executable: str,
22
           use_stdin: bool=False,
23
           use_stdout: bool=True,
24
           use_stderr: bool=False,
25
           config_suffix: str="",
26
           prerequisite_check_command: tuple=(),
27
           output_format: (str, None)=None,
28
           **options):
29
    """
30
    Decorator that creates a ``LocalBear`` that is able to process results from
31
    an external linter tool.
32
33
    The main functionality is achieved through the ``create_arguments()``
34
    function that constructs the command-line-arguments that get parsed to your
35
    executable.
36
37
    >>> @Linter("xlint", output_format="regex", output_regex="...")
38
    ... class XLintBear:
39
    ...     @staticmethod
40
    ...     def create_arguments(filename, file, config_file):
41
    ...         return "--lint", filename
42
43
    Requiring settings is possible like in ``Bear.run()`` with supplying
44
    additional keyword arguments (and if needed with defaults).
45
46
    >>> @Linter("xlint", output_format="regex", output_regex="...")
47
    ... class XLintBear:
48
    ...     @staticmethod
49
    ...     def create_arguments(filename,
50
    ...                          file,
51
    ...                          config_file,
52
    ...                          lintmode: str,
53
    ...                          enable_aggressive_lints: bool=False):
54
    ...         arguments = ("--lint", filename, "--mode=" + lintmode)
55
    ...         if enable_aggressive_lints:
56
    ...             arguments += ("--aggressive",)
57
    ...         return arguments
58
59
    Sometimes your tool requires an actual file that contains configuration.
60
    ``Linter`` allows you to just define the contents the configuration shall
61
    contain via ``generate_config()`` and handles everything else for you.
62
63
    >>> @Linter("xlint", output_format="regex", output_regex="...")
64
    ... class XLintBear:
65
    ...     @staticmethod
66
    ...     def generate_config(filename,
67
    ...                         file,
68
    ...                         lintmode,
69
    ...                         enable_aggressive_lints):
70
    ...         modestring = ("aggressive"
71
    ...                       if enable_aggressive_lints else
72
    ...                       "non-aggressive")
73
    ...         contents = ("<xlint>",
74
    ...                     "    <mode>" + lintmode + "</mode>",
75
    ...                     "    <aggressive>" + modestring + "</aggressive>",
76
    ...                     "</xlint>")
77
    ...         return "\\n".join(contents)
78
    ...
79
    ...     @staticmethod
80
    ...     def create_arguments(filename,
81
    ...                          file,
82
    ...                          config_file):
83
    ...         return "--lint", filename, "--config", config_file
84
85
    As you can see you don't need to copy additional keyword-arguments you
86
    introduced from ``create_arguments()`` to ``generate_config()`` and
87
    vice-versa. ``Linter`` takes care of forwarding the right arguments to the
88
    right place, so you are able to avoid signature duplication.
89
90
    If you override ``process_output``, you have the same feature like above
91
    (auto-forwarding of the right arguments defined in your function
92
    signature).
93
94
    Note when overriding ``process_output``: Providing a single output stream
95
    (via ``use_stdout`` or ``use_stderr``) puts the according string attained
96
    from the stream into parameter ``output``, providing both output streams
97
    inputs a tuple with ``(stdout, stderr)``. Generally providing no stream
98
    raises a ``ValueError``.
99
100
    Documentation:
101
    Bear description shall be provided at class level.
102
    If you document your additional parameters inside ``create_arguments``,
103
    ``generate_config`` and ``process_output``, beware that conflicting
104
    documentation between them may be overridden. Document duplicated
105
    parameters inside ``create_arguments`` first, then in ``generate_config``
106
    and after that inside ``process_output``.
107
108
    For the tutorial see:
109
    http://coala.readthedocs.org/en/latest/Users/Tutorials/Linter_Bears.html
110
111
    :param executable:
112
        The linter tool.
113
    :param use_stdin:
114
        Whether the input file is sent via stdin instead of passing it over the
115
        command-line-interface.
116
    :param use_stdout:
117
        Whether to use the stdout output stream.
118
    :param use_stderr:
119
        Whether to use the stderr output stream.
120
    :param config_suffix:
121
        The suffix-string to append to the filename of the configuration file
122
        created when ``generate_config`` is supplied. Useful if your executable
123
        expects getting a specific file-type with specific file-ending for the
124
        configuration file.
125
    :param prerequisite_check_command:
126
        A custom command to check for when ``check_prerequisites`` gets
127
        invoked (via ``subprocess.check_call()``). Must be an ``Iterable``.
128
    :param prerequisite_check_fail_message:
129
        A custom command to check for when ``check_prerequisites`` gets
130
        invoked. Must be provided only together with
131
        ``prerequisite_check_command``.
132
    :param output_format:
133
        The output format of the underlying executable. Valid values are
134
135
        - ``None``: Define your own format by overriding ``process_output``.
136
          Overriding ``process_output`` is then mandatory, not specifying it
137
          raises a ``ValueError``.
138
        - ``'regex'``: Parse output using a regex. See parameter
139
          ``output_regex``.
140
        - ``'corrected'``: The output is the corrected of the given file. Diffs
141
          are then generated to supply patches for results.
142
143
        Passing something else raises a ``ValueError``.
144
    :param output_regex:
145
        The regex expression as a string that is used to parse the output
146
        generated by the underlying executable. It should use as many of the
147
        following named groups (via ``(?P<name>...)``) to provide a good
148
        result:
149
150
        - line - The line where the issue starts.
151
        - column - The column where the issue starts.
152
        - end_line - The line where the issue ends.
153
        - end_column - The column where the issue ends.
154
        - severity - The severity of the issue.
155
        - message - The message of the result.
156
        - origin - The origin of the issue.
157
158
        The groups ``line``, ``column``, ``end_line`` and ``end_column`` don't
159
        have to match numbers only, they can also match nothing, the generated
160
        ``Result`` is filled automatically with ``None`` then for the
161
        appropriate properties.
162
163
        Needs to be provided if ``output_format`` is ``'regex'``.
164
    :param severity_map:
165
        A dict used to map a severity string (captured from the
166
        ``output_regex`` with the named group ``severity``) to an actual
167
        ``coalib.results.RESULT_SEVERITY`` for a result. Severity strings are
168
        mapped **case-insensitive**!
169
170
        - ``RESULT_SEVERITY.MAJOR``: Mapped by ``error``.
171
        - ``RESULT_SEVERITY.NORMAL``: Mapped by ``warning`` or ``warn``.
172
        - ``RESULT_SEVERITY.MINOR``: Mapped by ``info``.
173
174
        A ``ValueError`` is raised when the named group ``severity`` is not
175
        used inside ``output_regex`` and this parameter is given.
176
    :param diff_severity:
177
        The severity to use for all results if ``output_format`` is
178
        ``'corrected'``. By default this value is
179
        ``coalib.results.RESULT_SEVERITY.NORMAL``. The given value needs to be
180
        defined inside ``coalib.results.RESULT_SEVERITY``.
181
    :param diff_message:
182
        The message-string to use for all results if ``output_format`` is
183
        ``'corrected'``. By default this value is ``"Inconsistency found."``.
184
    :raises ValueError:
185
        Raised when invalid options are supplied.
186
    :raises TypeError:
187
        Raised when incompatible types are supplied.
188
        See parameter documentations for allowed types.
189
    :return:
190
        A ``LocalBear`` derivation that lints code using an external tool.
191
    """
192
    options["executable"] = executable
193
    options["output_format"] = output_format
194
    options["use_stdin"] = use_stdin
195
    options["use_stdout"] = use_stdout
196
    options["use_stderr"] = use_stderr
197
    options["config_suffix"] = config_suffix
198
    options["prerequisite_check_command"] = prerequisite_check_command
199
200
    allowed_options = {"executable",
201
                       "output_format",
202
                       "use_stdin",
203
                       "use_stdout",
204
                       "use_stderr",
205
                       "config_suffix",
206
                       "prerequisite_check_command"}
207
208
    if not options["use_stdout"] and not options["use_stderr"]:
209
        raise ValueError("No output streams provided at all.")
210
211
    if options["output_format"] == "corrected":
212
        if "diff_severity" in options:
213
            if options["diff_severity"] not in RESULT_SEVERITY.reverse:
214
                raise TypeError("Invalid value for `diff_severity`: " +
215
                                repr(options["diff_severity"]))
216
217
        if "diff_message" in options:
218
            assert_right_type(options["diff_message"], str, "diff_message")
219
220
        allowed_options |= {"diff_severity", "diff_message"}
221
    elif options["output_format"] == "regex":
222
        if "output_regex" not in options:
223
            raise ValueError("No `output_regex` specified.")
224
225
        options["output_regex"] = re.compile(options["output_regex"])
226
227
        # Don't setup severity_map if one is provided by user or if it's not
228
        # used inside the output_regex. If one is manually provided but not
229
        # used in the output_regex, throw an exception.
230
        if "severity_map" in options:
231
            if "severity" not in options["output_regex"].groupindex:
232
                raise ValueError("Provided `severity_map` but named group "
233
                                 "`severity` is not used in `output_regex`.")
234
            assert_right_type(options["severity_map"], dict, "severity_map")
235
236
            for key, value in options["severity_map"].items():
237
                try:
238
                    assert_right_type(key, str, "<severity_map dict-key>")
239
                except TypeError:
240
                    raise TypeError("The key " + repr(key) + " inside given "
241
                                    "severity-map is no string.")
242
243
                try:
244
                    assert_right_type(value, int, "<severity_map dict-value>")
245
                except TypeError:
246
                    raise TypeError(
247
                        "The value {} for key {} inside given severity-map is "
248
                        "no valid severity value.".format(repr(value),
249
                                                          repr(key)))
250
251
                if value not in RESULT_SEVERITY.reverse:
252
                    raise TypeError(
253
                        "Invalid severity value {} for key {} inside given "
254
                        "severity-map.".format(repr(value), repr(key)))
255
256
            # Auto-convert keys to lower-case. This creates automatically a new
257
            # dict which prevents runtime-modifications.
258
            options["severity_map"] = {
259
                key.lower(): value
260
                for key, value in options["severity_map"].items()}
261
262
        allowed_options |= {"output_regex", "severity_map"}
263
    elif options["output_format"] is not None:
264
        raise ValueError("Invalid `output_format` specified.")
265
266
    if options["prerequisite_check_command"]:
267
        if "prerequisite_check_fail_message" in options:
268
            assert_right_type(options["prerequisite_check_fail_message"],
269
                              str,
270
                              "prerequisite_check_fail_message")
271
        else:
272
            options["prerequisite_check_fail_message"] = (
273
                "Prerequisite check failed.")
274
275
        allowed_options.add("prerequisite_check_fail_message")
276
277
    # Check for illegal superfluous options.
278
    superfluous_options = options.keys() - allowed_options
279
    if superfluous_options:
280
        raise ValueError(
281
            "Invalid keyword arguments provided: " +
282
            ", ".join(repr(s) for s in sorted(superfluous_options)))
283
284
    def create_linter(klass):
285
        class LinterMeta(type):
286
287
            def __repr__(self):
0 ignored issues
show
Coding Style Best Practice introduced by
The first argument of the metaclass method __repr__ should be named cls.
Loading history...
288
                return "<{} class (wrapping {}) at {}>".format(
289
                    self.__name__,
290
                    repr(options["executable"]),
291
                    hex(id(self)))
292
293
        class LinterBase(LocalBear, metaclass=LinterMeta):
0 ignored issues
show
Unused Code introduced by
This abstract class does not seem to be used anywhere.
Loading history...
294
295
            @staticmethod
296
            def generate_config(filename, file):
297
                """
298
                Generates the content of a config-file the linter-tool might
299
                need.
300
301
                The contents generated from this function are written to a
302
                temporary file and the path is provided inside
303
                ``create_arguments()``.
304
305
                By default no configuration is generated.
306
307
                You can provide additional keyword arguments and defaults.
308
                These will be interpreted as required settings that need to be
309
                provided through a coafile-section.
310
311
                :param filename:
312
                    The name of the file currently processed.
313
                :param file:
314
                    The contents of the file currently processed.
315
                :return:
316
                    The config-file-contents as a string or ``None``.
317
                """
318
                return None
319
320
            @staticmethod
321
            def create_arguments(filename, file, config_file):
322
                """
323
                Creates the arguments for the linter.
324
325
                You can provide additional keyword arguments and defaults.
326
                These will be interpreted as required settings that need to be
327
                provided through a coafile-section.
328
329
                :param filename:
330
                    The name of the file the linter-tool shall process.
331
                :param file:
332
                    The contents of the file.
333
                :param config_file:
334
                    The path of the config-file if used. ``None`` if unused.
335
                :return:
336
                    A sequence of arguments to feed the linter-tool with.
337
                """
338
                raise NotImplementedError
339
340
            @staticmethod
341
            def get_executable():
342
                """
343
                Returns the executable of this class.
344
345
                :return:
346
                    The executable name.
347
                """
348
                return options["executable"]
349
350
            @classmethod
351
            def check_prerequisites(cls):
352
                """
353
                Checks whether the linter-tool the bear uses is operational.
354
355
                :return:
356
                    True if available, otherwise a string containing more info.
357
                """
358
                if shutil.which(cls.get_executable()) is None:
359
                    return repr(cls.get_executable()) + " is not installed."
360
                else:
361
                    if options["prerequisite_check_command"]:
362
                        try:
363
                            check_call(options["prerequisite_check_command"],
364
                                       stdout=DEVNULL,
365
                                       stderr=DEVNULL)
366
                            return True
367
                        except (OSError, CalledProcessError):
368
                            return options["prerequisite_check_fail_message"]
369
                    return True
370
371
            @classmethod
372
            def _get_create_arguments_metadata(cls):
373
                return FunctionMetadata.from_function(
374
                    cls.create_arguments,
375
                    omit={"filename", "file", "config_file"})
376
377
            @classmethod
378
            def _get_generate_config_metadata(cls):
379
                return FunctionMetadata.from_function(
380
                    cls.generate_config,
381
                    omit={"filename", "file"})
382
383
            @classmethod
384
            def _get_process_output_metadata(cls):
385
                return FunctionMetadata.from_function(
386
                    cls.process_output,
387
                    omit={"self", "output", "filename", "file"})
388
389
            @classmethod
390
            def get_non_optional_settings(cls):
391
                return cls.get_metadata().non_optional_params
392
393
            @classmethod
394
            def get_metadata(cls):
395
                merged_metadata = FunctionMetadata.merge(
396
                    cls._get_process_output_metadata(),
397
                    cls._get_generate_config_metadata(),
398
                    cls._get_create_arguments_metadata())
399
                merged_metadata.desc = inspect.getdoc(cls)
400
                return merged_metadata
401
402
            @classmethod
403
            def _execute_command(cls, args, stdin=None):
404
                """
405
                Executes the underlying tool with the given arguments.
406
407
                :param args:
408
                    The argument sequence to pass to the executable.
409
                :param stdin:
410
                    Input to send to the opened process as stdin.
411
                :return:
412
                    A tuple with ``(stdout, stderr)``.
413
                """
414
                return run_shell_command(
415
                    (cls.get_executable(),) + tuple(args),
416
                    stdin=stdin)
417
418
            def _convert_output_regex_match_to_result(self,
419
                                                      match,
420
                                                      filename,
421
                                                      severity_map):
422
                """
423
                Converts the matched named-groups of ``output_regex`` to an
424
                actual ``Result``.
425
426
                :param match:
427
                    The regex match object.
428
                :param filename:
429
                    The name of the file this match belongs to.
430
                :param severity_map:
431
                    The dict to use to map the severity-match to an actual
432
                    ``RESULT_SEVERITY``.
433
                """
434
                # Pre process the groups
435
                groups = match.groupdict()
436
437
                try:
438
                    groups["severity"] = severity_map[
439
                        groups["severity"].lower()]
440
                except KeyError:
441
                    self.warn(
442
                        "No correspondence for " + repr(groups["severity"]) +
443
                        " found in given severity map. Assuming "
444
                        "`RESULT_SEVERITY.NORMAL`.")
445
                    groups["severity"] = RESULT_SEVERITY.NORMAL
446
447
                for variable in ("line", "column", "end_line", "end_column"):
448
                    groups[variable] = (None
449
                                        if groups.get(variable, "") == "" else
450
                                        int(groups[variable]))
451
452
                if "origin" in groups:
453
                    groups["origin"] = "{} ({})".format(
454
                        str(klass.__name__),
455
                        str(groups["origin"]))
456
457
                # Construct the result.
458
                return Result.from_values(
459
                    origin=groups.get("origin", self),
460
                    message=groups.get("message", ""),
461
                    file=filename,
462
                    severity=int(groups.get("severity",
463
                                            RESULT_SEVERITY.NORMAL)),
464
                    line=groups["line"],
465
                    column=groups["column"],
466
                    end_line=groups["end_line"],
467
                    end_column=groups["end_column"])
468
469
            def process_output_corrected(self,
470
                                         output,
471
                                         filename,
472
                                         file,
473
                                         diff_severity=RESULT_SEVERITY.NORMAL,
474
                                         diff_message="Inconsistency found."):
475
                """
476
                Processes the executable's output as a corrected file.
477
478
                :param output:
479
                    The output of the program. This can be either a single
480
                    string or a sequence of strings.
481
                :param filename:
482
                    The filename of the file currently being corrected.
483
                :param file:
484
                    The contents of the file currently being corrected.
485
                :param diff_severity:
486
                    The severity to use for generating results.
487
                :param diff_message:
488
                    The message to use for generating results.
489
                :return:
490
                    An iterator returning results containing patches for the
491
                    file to correct.
492
                """
493
                if isinstance(output, str):
494
                    output = (output,)
495
496
                for string in output:
497
                    for diff in Diff.from_string_arrays(
498
                                file,
499
                                string.splitlines(keepends=True)).split_diff():
500
                        yield Result(self,
501
                                     diff_message,
502
                                     affected_code=(diff.range(filename),),
503
                                     diffs={filename: diff},
504
                                     severity=diff_severity)
505
506
            def process_output_regex(
507
                    self,
508
                    output,
509
                    filename,
510
                    file,
511
                    output_regex,
512
                    severity_map=MappingProxyType({
513
                        "error": RESULT_SEVERITY.MAJOR,
514
                        "warning": RESULT_SEVERITY.NORMAL,
515
                        "warn": RESULT_SEVERITY.NORMAL,
516
                        "info": RESULT_SEVERITY.INFO})):
517
                """
518
                Processes the executable's output using a regex.
519
520
                :param output:
521
                    The output of the program. This can be either a single
522
                    string or a sequence of strings.
523
                :param filename:
524
                    The filename of the file currently being corrected.
525
                :param file:
526
                    The contents of the file currently being corrected.
527
                :param output_regex:
528
                    The regex to parse the output with. It should use as many
529
                    of the following named groups (via ``(?P<name>...)``) to
530
                    provide a good result:
531
532
                    - line - The line where the issue starts.
533
                    - column - The column where the issue starts.
534
                    - end_line - The line where the issue ends.
535
                    - end_column - The column where the issue ends.
536
                    - severity - The severity of the issue.
537
                    - message - The message of the result.
538
                    - origin - The origin of the issue.
539
540
                    The groups ``line``, ``column``, ``end_line`` and
541
                    ``end_column`` don't have to match numbers only, they can
542
                    also match nothing, the generated ``Result`` is filled
543
                    automatically with ``None`` then for the appropriate
544
                    properties.
545
                :param severity_map:
546
                    A dict used to map a severity string (captured from the
547
                    ``output_regex`` with the named group ``severity``) to an
548
                    actual ``coalib.results.RESULT_SEVERITY`` for a result.
549
                :return:
550
                    An iterator returning results.
551
                """
552
                if isinstance(output, str):
553
                    output = (output,)
554
555
                for string in output:
556
                    for match in re.finditer(output_regex, string):
557
                        yield self._convert_output_regex_match_to_result(
558
                            match, filename, severity_map=severity_map)
559
560
            if options["output_format"] is None:
561
                # Check if user supplied a `process_output` override.
562
                if not (hasattr(klass, "process_output") and
563
                        callable(klass.process_output)):
564
                    raise ValueError("`process_output` not provided by given "
565
                                     "class.")
566
                # No need to assign to `process_output` here, the class mixing
567
                # below automatically does that.
568
            else:
569
                # Prevent people from accidentally defining `process_output`
570
                # manually, as this would implicitly override the internally
571
                # set-up `process_output`.
572
                if hasattr(klass, "process_output"):
573
                    raise ValueError("`process_output` is used by given class,"
574
                                     " but " + repr(options["output_format"]) +
575
                                     " output format was specified.")
576
577
                if options["output_format"] == "corrected":
578
                    process_output_args = {}
579
                    if "diff_severity" in options:
580
                        process_output_args["diff_severity"] = (
581
                            options["diff_severity"])
582
                    if "diff_message" in options:
583
                        process_output_args["diff_message"] = (
584
                            options["diff_message"])
585
586
                    process_output = partialmethod(
587
                        process_output_corrected, **process_output_args)
588
589
                elif options["output_format"] == "regex":
590
                    process_output_args = {
591
                        "output_regex": options["output_regex"]}
592
                    if "severity_map" in options:
593
                        process_output_args["severity_map"] = (
594
                            options["severity_map"])
595
596
                    process_output = partialmethod(
597
                        process_output_regex, **process_output_args)
598
599
                else:  # pragma: no cover
600
                    # This statement is never reached.
601
                    # Due to a bug in coverage we can't use `pass` here, as
602
                    # the ignore-pragma doesn't take up this else-clause then.
603
                    # https://bitbucket.org/ned/coveragepy/issues/483/partial-
604
                    # branch-coverage-pragma-no-cover
605
                    assert False
606
607
            @classmethod
608
            @contextmanager
609
            def _create_config(cls, filename, file, **kwargs):
610
                """
611
                Provides a context-manager that creates the config file if the
612
                user provides one and cleans it up when done with linting.
613
614
                :param filename:
615
                    The filename of the file.
616
                :param file:
617
                    The file contents.
618
                :param kwargs:
619
                    Section settings passed from ``run()``.
620
                :return:
621
                    A context-manager handling the config-file.
622
                """
623
                content = cls.generate_config(filename, file, **kwargs)
624
                if content is None:
625
                    yield None
626
                else:
627
                    tmp_suffix = options["config_suffix"]
628
                    with make_temp(suffix=tmp_suffix) as config_file:
629
                        with open(config_file, mode="w") as fl:
630
                            fl.write(content)
631
                        yield config_file
632
633
            @staticmethod
634
            def _filter_kwargs(metadata, kwargs):
635
                """
636
                Filter out kwargs using the given metadata. Means only
637
                parameters contained in the metadata specification are taken
638
                from kwargs and returned.
639
640
                :param metadata:
641
                    The signature specification.
642
                :param kwargs:
643
                    The kwargs to filter.
644
                :return:
645
                    The filtered kwargs.
646
                """
647
                return {key: kwargs[key]
648
                        for key in metadata.non_optional_params.keys() |
649
                        metadata.optional_params.keys()
650
                        if key in kwargs}
651
652
            def run(self, filename, file, **kwargs):
653
                # Get the **kwargs params to forward to `generate_config()`
654
                # (from `_create_config()`).
655
                generate_config_kwargs = self._filter_kwargs(
656
                    self._get_generate_config_metadata(), kwargs)
657
658
                with self._create_config(
659
                        filename,
660
                        file,
661
                        **generate_config_kwargs) as config_file:
662
663
                    # And now retrieve the **kwargs for `create_arguments()`.
664
                    create_arguments_kwargs = self._filter_kwargs(
665
                        self._get_create_arguments_metadata(), kwargs)
666
667
                    output = self._execute_command(
668
                        self.create_arguments(filename,
669
                                              file,
670
                                              config_file,
671
                                              **create_arguments_kwargs),
672
                        stdin="".join(file) if options["use_stdin"] else None)
673
                    output = tuple(compress(
674
                        output,
675
                        (options["use_stdout"], options["use_stderr"])))
676
                    if len(output) == 1:
677
                        output = output[0]
678
679
                    process_output_kwargs = self._filter_kwargs(
680
                        self._get_process_output_metadata(), kwargs)
681
                    return self.process_output(output, filename, file,
682
                                               **process_output_kwargs)
683
684
        # Mixin the linter into the user-defined interface, otherwise
685
        # `create_arguments` and other methods would be overridden by the
686
        # default version.
687
        return type(klass.__name__, (klass, LinterBase), {})
688
689
    return create_linter
690