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

LinterBase   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 372
Duplicated Lines 0 %
Metric Value
dl 0
loc 372
rs 8.3673
wmc 45

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