# 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