usse/scrape/venv/lib/python3.10/site-packages/esbonio/lsp/log.py
2023-12-22 15:26:01 +01:00

181 lines
5.1 KiB
Python

import logging
import pathlib
import traceback
import typing
from typing import List
from typing import Tuple
import pygls.uris as uri
from lsprotocol.types import Diagnostic
from lsprotocol.types import DiagnosticSeverity
from lsprotocol.types import DiagnosticTag
from lsprotocol.types import Position
from lsprotocol.types import Range
if typing.TYPE_CHECKING:
from .rst import RstLanguageServer
from .rst.config import ServerConfig
LOG_NAMESPACE = "esbonio.lsp"
LOG_LEVELS = {
"debug": logging.DEBUG,
"error": logging.ERROR,
"info": logging.INFO,
}
class LogFilter(logging.Filter):
"""A log filter that accepts message from any of the listed logger names."""
def __init__(self, names):
self.names = names
def filter(self, record):
return any(record.name == name for name in self.names)
class MemoryHandler(logging.Handler):
"""A logging handler that caches messages in memory."""
def __init__(self):
super().__init__()
self.records: List[logging.LogRecord] = []
def emit(self, record: logging.LogRecord) -> None:
self.records.append(record)
class LspHandler(logging.Handler):
"""A logging handler that will send log records to an LSP client."""
def __init__(
self, server: "RstLanguageServer", show_deprecation_warnings: bool = False
):
super().__init__()
self.server = server
self.show_deprecation_warnings = show_deprecation_warnings
def get_warning_path(self, warning: str) -> Tuple[str, List[str]]:
"""Determine the filepath that the warning was emitted from."""
path, *parts = warning.split(":")
# On windows the rest of the path will be in the first element of parts.
if pathlib.Path(warning).drive:
path += f":{parts.pop(0)}"
return path, parts
def handle_warning(self, record: logging.LogRecord):
"""Publish warnings to the client as diagnostics."""
if not isinstance(record.args, tuple):
self.server.logger.debug(
"Unable to handle warning, expected tuple got: %s", record.args
)
return
# The way warnings are logged is different in Python 3.11+
if len(record.args) == 0:
argument = record.msg
else:
argument = record.args[0] # type: ignore
if not isinstance(argument, str):
self.server.logger.debug(
"Unable to handle warning, expected string got: %s", argument
)
return
warning, *_ = argument.split("\n")
path, (linenum, category, *msg) = self.get_warning_path(warning)
category = category.strip()
message = ":".join(msg).strip()
try:
line = int(linenum)
except ValueError:
line = 1
self.server.logger.debug(
"Unable to parse line number: '%s'\n%s", linenum, traceback.format_exc()
)
tags = []
if category == "DeprecationWarning":
tags.append(DiagnosticTag.Deprecated)
diagnostic = Diagnostic(
range=Range(
start=Position(line=line - 1, character=0),
end=Position(line=line, character=0),
),
message=message,
severity=DiagnosticSeverity.Warning,
tags=tags,
)
self.server.add_diagnostics("esbonio", uri.from_fs_path(path), diagnostic)
self.server.sync_diagnostics()
def emit(self, record: logging.LogRecord) -> None:
"""Sends the record to the client."""
# To avoid infinite recursions, it's simpler to just ignore all log records
# coming from pygls...
if "pygls" in record.name:
return
if record.name == "py.warnings":
if not self.show_deprecation_warnings:
return
self.handle_warning(record)
log = self.format(record).strip()
self.server.show_message_log(log)
def setup_logging(server: "RstLanguageServer", config: "ServerConfig"):
"""Setup logging to route log messages to the language client as
``window/logMessage`` messages.
Parameters
----------
server
The server to use to send messages
config
The configuration to use
"""
level = LOG_LEVELS[config.log_level]
warnlog = logging.getLogger("py.warnings")
logger = logging.getLogger(LOG_NAMESPACE)
logger.setLevel(level)
lsp_handler = LspHandler(server, config.show_deprecation_warnings)
lsp_handler.setLevel(level)
if len(config.log_filter) > 0:
lsp_handler.addFilter(LogFilter(config.log_filter))
formatter = logging.Formatter("[%(name)s] %(message)s")
lsp_handler.setFormatter(formatter)
# Look to see if there are any cached messages we should forward to the client.
for handler in logger.handlers:
if not isinstance(handler, MemoryHandler):
continue
for record in handler.records:
if logger.isEnabledFor(record.levelno):
lsp_handler.emit(record)
logger.removeHandler(handler)
logger.addHandler(lsp_handler)
warnlog.addHandler(lsp_handler)