Python Language Server Protocol implementation providing code intelligence features for Python development
—
Comprehensive utility functions for LSP operations, text manipulation, URI handling, Python-specific operations, and code formatting. Provides essential helper functions for plugin development and server operations.
Utility decorators for controlling function execution timing and frequency.
def debounce(interval_s, keyed_by=None):
"""
Debounce function calls until interval_s seconds have passed.
Parameters:
- interval_s: float, debounce interval in seconds
- keyed_by: str, parameter name to key debouncing by
Returns:
decorator: Function decorator
"""
def throttle(seconds=1):
"""
Throttle function calls to at most once per interval.
Parameters:
- seconds: float, minimum interval between calls
Returns:
decorator: Function decorator
"""Functions for finding and manipulating file paths.
def find_parents(root, path, names):
"""
Find files matching names in parent directories.
Parameters:
- root: str, root directory to stop searching at
- path: str, starting path for search
- names: list, file/directory names to find
Returns:
list: Found file paths
"""
def path_to_dot_name(path):
"""
Convert filesystem path to Python dot notation.
Parameters:
- path: str, filesystem path to Python file
Returns:
str: Python module path in dot notation
"""
def match_uri_to_workspace(uri, workspaces):
"""
Match URI to appropriate workspace.
Parameters:
- uri: str, document URI
- workspaces: list, available Workspace instances
Returns:
Workspace: Best matching workspace
"""Functions for working with data structures and strings.
def list_to_string(value):
"""
Convert list to comma-separated string.
Parameters:
- value: list or str, value to convert
Returns:
str: Comma-separated string
"""
def merge_dicts(dict_a, dict_b):
"""
Recursively merge dictionaries.
Parameters:
- dict_a: dict, base dictionary
- dict_b: dict, dictionary to merge in
Returns:
dict: Merged dictionary
"""Functions for formatting text content for LSP clients.
def escape_plain_text(contents):
"""
Escape text for plain text display.
Parameters:
- contents: str, text to escape
Returns:
str: Escaped text
"""
def escape_markdown(contents):
"""
Escape text for Markdown display.
Parameters:
- contents: str, text to escape
Returns:
str: Escaped Markdown text
"""
def wrap_signature(signature):
"""
Wrap function signature in code block.
Parameters:
- signature: str, function signature
Returns:
str: Wrapped signature
"""
def choose_markup_kind(client_supported_markup_kinds):
"""
Choose best supported markup kind for client.
Parameters:
- client_supported_markup_kinds: list, client-supported markup kinds
Returns:
str: Best markup kind to use
"""
def format_docstring(contents, markup_kind, signatures=None, signature_config=None):
"""
Format docstring as LSP MarkupContent.
Parameters:
- contents: str, docstring content
- markup_kind: str, markup kind ("plaintext" or "markdown")
- signatures: list, optional function signatures
- signature_config: dict, signature formatting options
Returns:
dict: LSP MarkupContent with 'kind' and 'value'
"""Functions for working with LSP positions and text coordinates.
def clip_column(column, lines, line_number):
"""
Normalize column position within line bounds.
Parameters:
- column: int, column position
- lines: list, document lines
- line_number: int, line number (0-based)
Returns:
int: Clipped column position
"""
def position_to_jedi_linecolumn(document, position):
"""
Convert LSP position to Jedi line/column format.
Parameters:
- document: Document, document instance
- position: dict, LSP position with 'line' and 'character'
Returns:
tuple: (line, column) for Jedi (1-based line, 0-based column)
"""
def get_eol_chars(text):
"""
Get end-of-line characters used in text.
Parameters:
- text: str, text to analyze
Returns:
str: EOL characters ("\r\n", "\r", or "\n") or None if none found
"""
def format_signature(signature, config, signature_formatter):
"""
Format function signature using ruff or black formatter.
Parameters:
- signature: str, function signature to format
- config: dict, formatting configuration with line_length
- signature_formatter: str, formatter name ("ruff" or "black")
Returns:
str: Formatted signature
"""
def convert_signatures_to_markdown(signatures, config):
"""
Convert list of signatures to markdown code block.
Parameters:
- signatures: list, function signatures
- config: dict, formatting configuration
Returns:
str: Markdown-formatted signatures
"""Functions for process management and monitoring.
def is_process_alive(pid):
"""
Check if process is still alive.
Parameters:
- pid: int, process ID
Returns:
bool: True if process is alive
"""Functions for working with URIs and filesystem paths.
def urlparse(uri):
"""
Parse and decode URI parts.
Parameters:
- uri: str, URI to parse
Returns:
tuple: Parsed URI components (scheme, netloc, path, params, query, fragment)
"""
def urlunparse(parts):
"""
Unparse and encode URI parts.
Parameters:
- parts: tuple, URI components
Returns:
str: Assembled URI
"""
def to_fs_path(uri):
"""
Convert URI to filesystem path.
Parameters:
- uri: str, file URI
Returns:
str: Filesystem path
"""
def from_fs_path(path):
"""
Convert filesystem path to URI.
Parameters:
- path: str, filesystem path
Returns:
str: File URI
"""
def uri_with(uri, **parts):
"""
Return URI with specified parts replaced.
Parameters:
- uri: str, base URI
- **parts: URI components to replace
Returns:
str: Modified URI
"""Functions for manipulating and applying text edits.
def get_well_formatted_range(lsp_range):
"""
Normalize LSP range format.
Parameters:
- lsp_range: dict, LSP range with 'start' and 'end'
Returns:
dict: Normalized range
"""
def get_well_formatted_edit(text_edit):
"""
Normalize LSP text edit format.
Parameters:
- text_edit: dict, LSP TextEdit
Returns:
dict: Normalized text edit
"""
def compare_text_edits(a, b):
"""
Compare text edits for sorting.
Parameters:
- a: dict, first text edit
- b: dict, second text edit
Returns:
int: Comparison result (-1, 0, 1)
"""
def merge_sort_text_edits(text_edits):
"""
Sort text edits by position.
Parameters:
- text_edits: list, text edits to sort
Returns:
list: Sorted text edits
"""
def apply_text_edits(doc, text_edits):
"""
Apply text edits to document.
Parameters:
- doc: Document, document to edit
- text_edits: list, text edits to apply
Returns:
str: Document text after applying edits
"""Classes for code formatting with different tools.
class Formatter:
"""Base class for code formatters."""
@property
def is_installed(self):
"""
Check if formatter is available.
Returns:
bool: True if formatter is installed
"""
def format(self, code, line_length=79):
"""
Format code.
Parameters:
- code: str, code to format
- line_length: int, maximum line length
Returns:
str: Formatted code
"""
class RuffFormatter(Formatter):
"""Ruff code formatter."""
class BlackFormatter(Formatter):
"""Black code formatter."""Utility constants used throughout the server.
JEDI_VERSION = "0.19.1" # Jedi library version
# End-of-line character sequences in preference order
EOL_CHARS = ["\r\n", "\r", "\n"]
# Regex for matching EOL characters
EOL_REGEX = re.compile(r"(\r\n|\r|\n)")
# Markup kinds supported by server
SERVER_SUPPORTED_MARKUP_KINDS = {"plaintext", "markdown"}
# Available formatters
formatters = {
"ruff": RuffFormatter(),
"black": BlackFormatter()
}Custom exceptions for text editing operations.
class OverLappingTextEditException(Exception):
"""Raised when text edits overlap and cannot be applied."""from pylsp._utils import debounce, throttle
# Debounce linting to avoid excessive calls
@debounce(0.5, keyed_by='document')
def run_linting(document):
# Linting logic here
pass
# Throttle progress updates
@throttle(seconds=1)
def update_progress(message):
# Progress update logic
passfrom pylsp._utils import find_parents, path_to_dot_name
# Find configuration files
config_files = find_parents(
"/project",
"/project/src/module.py",
["setup.cfg", "pyproject.toml"]
)
# Convert path to module name
module_name = path_to_dot_name("/project/src/utils/helper.py")
print(module_name) # "src.utils.helper"from pylsp._utils import (
format_docstring, escape_markdown, wrap_signature,
format_signature, convert_signatures_to_markdown
)
# Format docstring for hover
docstring = format_docstring(
"Function description\n\nArgs:\n x: parameter",
"markdown",
signatures=["def func(x: int) -> str"]
)
# Escape markdown content
escaped = escape_markdown("Text with *special* chars")
# Wrap signature
wrapped = wrap_signature("def example(param: str) -> int")
# Format signature with black or ruff
config = {"line_length": 88}
formatted_sig = format_signature("def func(a,b,c):", config, "black")
# Convert multiple signatures to markdown
sigs = ["def func1(x: int) -> str", "def func2(y: float) -> bool"]
markdown_sigs = convert_signatures_to_markdown(sigs, config)from pylsp._utils import position_to_jedi_linecolumn, clip_column
# Convert LSP position to Jedi format
lsp_pos = {"line": 5, "character": 10}
jedi_line, jedi_col = position_to_jedi_linecolumn(document, lsp_pos)
# Clip column to line bounds
clipped_col = clip_column(100, document.lines, 5) # Won't exceed line lengthfrom pylsp.uris import to_fs_path, from_fs_path, uri_with
# Convert between URIs and paths
path = to_fs_path("file:///project/src/main.py") # "/project/src/main.py"
uri = from_fs_path("/project/src/main.py") # "file:///project/src/main.py"
# Modify URI components
new_uri = uri_with("file:///project/old.py", path="/project/new.py")from pylsp.text_edit import apply_text_edits, merge_sort_text_edits
# Apply text edits to document
edits = [
{
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 5}
},
"newText": "print"
}
]
# Sort edits and apply
sorted_edits = merge_sort_text_edits(edits)
new_text = apply_text_edits(document, sorted_edits)from pylsp._utils import formatters
# Use available formatters
black_formatter = formatters["black"]
if black_formatter.is_installed:
formatted_code = black_formatter.format("def f():pass", line_length=88)
ruff_formatter = formatters["ruff"]
if ruff_formatter.is_installed:
formatted_code = ruff_formatter.format(code, line_length=100)from pylsp._utils import merge_dicts, list_to_string
# Merge configuration dictionaries
base_config = {"plugins": {"pyflakes": {"enabled": True}}}
user_config = {"plugins": {"pyflakes": {"ignore": ["E203"]}}}
merged = merge_dicts(base_config, user_config)
# Convert list to string
error_codes = ["E203", "W503", "E501"]
ignore_string = list_to_string(error_codes) # "E203,W503,E501"Install with Tessl CLI
npx tessl i tessl/pypi-python-lsp-server