File: pydocstyle_lint.py

package info (click to toggle)
python-lsp-server 1.12.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 796 kB
  • sloc: python: 7,791; sh: 12; makefile: 4
file content (127 lines) | stat: -rw-r--r-- 4,172 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
# Copyright 2017-2020 Palantir Technologies, Inc.
# Copyright 2021- Python Language Server Contributors.

import contextlib
import logging
import os
import re
import sys

import pydocstyle

from pylsp import hookimpl, lsp

log = logging.getLogger(__name__)

# PyDocstyle is a little verbose in debug message
pydocstyle_logger = logging.getLogger(pydocstyle.utils.__name__)
pydocstyle_logger.setLevel(logging.INFO)

DEFAULT_MATCH_RE = pydocstyle.config.ConfigurationParser.DEFAULT_MATCH_RE
DEFAULT_MATCH_DIR_RE = pydocstyle.config.ConfigurationParser.DEFAULT_MATCH_DIR_RE


@hookimpl
def pylsp_settings():
    # Default pydocstyle to disabled
    return {"plugins": {"pydocstyle": {"enabled": False}}}


@hookimpl
def pylsp_lint(config, workspace, document):
    with workspace.report_progress("lint: pydocstyle"):
        settings = config.plugin_settings("pydocstyle", document_path=document.path)
        log.debug("Got pydocstyle settings: %s", settings)

        # Explicitly passing a path to pydocstyle means it doesn't respect the --match flag, so do it ourselves
        filename_match_re = re.compile(settings.get("match", DEFAULT_MATCH_RE) + "$")
        if not filename_match_re.match(os.path.basename(document.path)):
            return []

        # Likewise with --match-dir
        dir_match_re = re.compile(settings.get("matchDir", DEFAULT_MATCH_DIR_RE) + "$")
        if not dir_match_re.match(os.path.basename(os.path.dirname(document.path))):
            return []

        args = [document.path]

        if settings.get("convention"):
            args.append("--convention=" + settings["convention"])

            if settings.get("addSelect"):
                args.append("--add-select=" + ",".join(settings["addSelect"]))
            if settings.get("addIgnore"):
                args.append("--add-ignore=" + ",".join(settings["addIgnore"]))

        elif settings.get("select"):
            args.append("--select=" + ",".join(settings["select"]))
        elif settings.get("ignore"):
            args.append("--ignore=" + ",".join(settings["ignore"]))

        log.info("Using pydocstyle args: %s", args)

        conf = pydocstyle.config.ConfigurationParser()
        with _patch_sys_argv(args):
            # TODO(gatesn): We can add more pydocstyle args here from our pylsp config
            conf.parse()

        # Will only yield a single filename, the document path
        diags = []
        for (
            filename,
            checked_codes,
            ignore_decorators,
            property_decorators,
            ignore_self_only_init,
        ) in conf.get_files_to_check():
            errors = pydocstyle.checker.ConventionChecker().check_source(
                document.source,
                filename,
                ignore_decorators=ignore_decorators,
                property_decorators=property_decorators,
                ignore_self_only_init=ignore_self_only_init,
            )

            try:
                for error in errors:
                    if error.code not in checked_codes:
                        continue
                    diags.append(_parse_diagnostic(document, error))
            except pydocstyle.parser.ParseError:
                # In the case we cannot parse the Python file, just continue
                pass

        log.debug("Got pydocstyle errors: %s", diags)
        return diags


def _parse_diagnostic(document, error):
    lineno = error.definition.start - 1
    line = document.lines[0] if document.lines else ""

    start_character = len(line) - len(line.lstrip())
    end_character = len(line)

    return {
        "source": "pydocstyle",
        "code": error.code,
        "message": error.message,
        "severity": lsp.DiagnosticSeverity.Warning,
        "range": {
            "start": {"line": lineno, "character": start_character},
            "end": {"line": lineno, "character": end_character},
        },
    }


@contextlib.contextmanager
def _patch_sys_argv(arguments) -> None:
    old_args = sys.argv

    # Preserve argv[0] since it's the executable
    sys.argv = old_args[0:1] + arguments

    try:
        yield
    finally:
        sys.argv = old_args